Deploying an Ember.js App
Ember.js is a powerful, opinionated JavaScript framework designed for building ambitious web applications with a strong focus on developer productivity and code maintainability. It features a robust CLI, convention-over-configuration philosophy, two-way data binding, a comprehensive router, built-in state management, and a large ecosystem of add-ons. Ember.js is ideal for building scalable single-page applications (SPAs) that require sophisticated client-side logic, complex data relationships, and long-term maintainability.
This comprehensive guide walks through deploying an Ember.js application to Klutch.sh using either Nixpacks (automatic zero-configuration deployment) or a Dockerfile (manual container control). You’ll learn how to scaffold an Ember project, create components and services, structure routes and data models, configure environment variables, implement security best practices, set up monitoring, deploy custom domains, and troubleshoot common issues. By the end of this guide, you’ll have a production-ready Ember.js application running on Klutch.sh’s global infrastructure with automatic HTTPS and optimized performance.
Prerequisites
- Node.js & npm (version 16+) – Download Node.js
- Ember CLI installed globally via npm
- Git installed locally and a GitHub account (Klutch.sh uses GitHub as the only git source)
- Klutch.sh account with access to the dashboard at klutch.sh/app
- Basic knowledge of JavaScript, Handlebars templates, and the Node.js ecosystem
Getting Started: Create an Ember.js App
1. Install Ember CLI
Install the Ember CLI globally:
npm install -g ember-cli2. Create a New Ember Project
Create a new Ember.js application:
ember new my-ember-appcd my-ember-appnpm install3. Project Structure
A typical Ember.js project structure looks like:
my-ember-app/├── app/│ ├── components/│ │ ├── article-list.hbs│ │ ├── article-list.js│ │ ├── article-item.hbs│ │ └── article-item.js│ ├── controllers/│ │ └── articles.js│ ├── models/│ │ ├── article.js│ │ └── user.js│ ├── routes/│ │ ├── articles.js│ │ ├── articles/detail.js│ │ └── index.js│ ├── services/│ │ ├── api.js│ │ └── auth.js│ ├── styles/│ │ └── app.css│ ├── templates/│ │ ├── application.hbs│ │ ├── articles.hbs│ │ ├── articles/detail.hbs│ │ └── index.hbs│ └── app.js├── config/│ └── environment.js├── public/├── tests/├── ember-cli-build.js├── package.json├── Dockerfile└── README.md4. Run the Development Server
Start the Ember development server:
ember serveNavigate to http://localhost:4200 in your browser. The app will automatically reload as you make changes.
5. Create a Data Model
Create an Ember data model for articles:
import Model, { attr } from '@ember-data/model';
export default class ArticleModel extends Model { @attr title; @attr content; @attr author; @attr('date') createdAt; @attr('date') updatedAt;
get shortContent() { return this.content.substring(0, 100) + '...'; }}6. Create a Service
Create a service for API communication:
import Service from '@ember/service';import { service } from '@ember/service';import fetch from 'fetch';
export default class ApiService extends Service { @service store;
constructor() { super(...arguments); this.baseURL = process.env.API_URL || 'http://localhost:3000/api'; }
async getArticles() { try { const response = await fetch(`${this.baseURL}/articles`); if (!response.ok) { throw new Error(`Failed to fetch articles: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error fetching articles:', error); throw error; } }
async getArticle(id) { try { const response = await fetch(`${this.baseURL}/articles/${id}`); if (!response.ok) { throw new Error(`Failed to fetch article: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error fetching article:', error); throw error; } }
async createArticle(data) { try { const response = await fetch(`${this.baseURL}/articles`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getToken()}` }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`Failed to create article: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error creating article:', error); throw error; } }
async updateArticle(id, data) { try { const response = await fetch(`${this.baseURL}/articles/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getToken()}` }, body: JSON.stringify(data) }); if (!response.ok) { throw new Error(`Failed to update article: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('Error updating article:', error); throw error; } }
async deleteArticle(id) { try { const response = await fetch(`${this.baseURL}/articles/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.getToken()}` } }); if (!response.ok) { throw new Error(`Failed to delete article: ${response.statusText}`); } } catch (error) { console.error('Error deleting article:', error); throw error; } }
getToken() { return localStorage.getItem('authToken') || ''; }}7. Create a Route
Create a route for articles:
import Route from '@ember/routing/route';import { service } from '@ember/service';
export default class ArticlesRoute extends Route { @service api; @service store;
async model() { try { const articles = await this.api.getArticles(); return this.store.push({ data: articles.map(article => ({ type: 'article', id: article.id, attributes: article })) }); } catch (error) { console.error('Error loading articles:', error); return []; } }}8. Create a Component
Create a reusable article list component:
import Component from '@glimmer/component';import { action } from '@ember/object';import { service } from '@ember/service';
export default class ArticleListComponent extends Component { @service api; @service router;
@action async deleteArticle(articleId) { if (confirm('Are you sure you want to delete this article?')) { try { await this.api.deleteArticle(articleId); // Refresh articles list this.router.transitionTo('articles'); } catch (error) { console.error('Error deleting article:', error); } } }
@action editArticle(articleId) { this.router.transitionTo('articles.edit', articleId); }
@action viewArticle(articleId) { this.router.transitionTo('articles.detail', articleId); }}Create the template:
{{! app/templates/components/article-list.hbs }}<div class="article-list"> <h2>Articles</h2>
{{#if (gt @articles.length 0)}} <div class="articles-grid"> {{#each @articles as |article|}} <div class="article-card"> <h3>{{article.title}}</h3> <p class="excerpt">{{article.shortContent}}</p> <div class="metadata"> <span class="author">By {{article.author}}</span> <span class="date">{{format-date article.createdAt}}</span> </div> <div class="actions"> <button {{on "click" (fn this.viewArticle article.id)}} class="btn btn-primary"> Read More </button> <button {{on "click" (fn this.editArticle article.id)}} class="btn btn-secondary"> Edit </button> <button {{on "click" (fn this.deleteArticle article.id)}} class="btn btn-danger"> Delete </button> </div> </div> {{/each}} </div> {{else}} <p class="no-articles">No articles found</p> {{/if}}</div>9. Router Configuration
Set up your routing in app/router.js:
import EmberRouter from '@ember/routing/router';import config from 'my-ember-app/config/environment';
export default class Router extends EmberRouter { location = config.locationType; rootURL = config.rootURL;}
Router.map(function () { this.route('articles', function () { this.route('detail', { path: ':id' }); this.route('edit', { path: ':id/edit' }); this.route('new'); }); this.route('about'); this.route('contact');});10. Application Template
Set up your main application template:
{{! app/templates/application.hbs }}<nav class="navbar"> <div class="navbar-brand"> <h1>My Ember App</h1> </div> <ul class="navbar-nav"> <li>{{#link-to "index"}}Home{{/link-to}}</li> <li>{{#link-to "articles"}}Articles{{/link-to}}</li> <li>{{#link-to "about"}}About{{/link-to}}</li> <li>{{#link-to "contact"}}Contact{{/link-to}}</li> </ul></nav>
<main class="container"> {{outlet}}</main>
<footer class="footer"> <p>© 2024 My Ember App. All rights reserved.</p></footer>11. Environment Configuration
Configure your app in config/environment.js:
'use strict';
module.exports = function (environment) { let ENV = { modulePrefix: 'my-ember-app', environment, rootURL: '/', locationType: 'history', EmberENV: { FEATURES: { // feature flags here }, EXTEND_PROTOTYPES: { Date: false, }, },
APP: { // API configuration apiURL: process.env.API_URL || 'http://localhost:3000/api', siteName: process.env.SITE_NAME || 'My Ember App' }, };
if (environment === 'development') { // Development-specific configuration ENV.APP.LOG_RESOLVER = true; ENV.APP.LOG_ACTIVE_GENERATION = true; ENV.APP.LOG_TRANSITIONS = true; ENV.APP.LOG_TRANSITIONS_INTERNAL = true; }
if (environment === 'test') { ENV.locationType = 'none'; ENV.APP.autoboot = false; }
if (environment === 'production') { // Production-specific configuration ENV.APP.analytics = process.env.ANALYTICS_ID; }
return ENV;};12. Package Scripts
Ensure your package.json has the necessary scripts:
{ "scripts": { "start": "ember serve", "build": "ember build --environment=production", "test": "ember test", "lint": "eslint .", "ember": "ember" }}Local Production Build Test
Before deploying, test the production build locally:
npm run buildember serve --environment=productionVisit http://localhost:4200 to verify that your app renders correctly in production mode.
Deploying with Nixpacks
Nixpacks automatically detects your Node.js/Ember.js application and configures build and runtime environments without requiring a Dockerfile. This is the simplest deployment method for Ember.js applications.
Prerequisites for Nixpacks Deployment
- Your Ember.js project pushed to a GitHub repository
- Valid
package.jsonwith build and start scripts - No
Dockerfilein the repository root (if one exists, Klutch.sh will use Docker instead)
Steps to Deploy with Nixpacks
-
Push Your Ember.js Project to GitHub
Initialize and push your project to GitHub if you haven’t already:
Terminal window git initgit add .git commit -m "Initial Ember.js app"git branch -M maingit remote add origin git@github.com:YOUR_USERNAME/YOUR_REPO.gitgit push -u origin main -
Log In to Klutch.sh Dashboard
Go to klutch.sh/app and sign in with your GitHub account.
-
Create a Project
Navigate to the Projects section and create a new project for your Ember.js app.
-
Create an App
Click “Create App” and select your GitHub repository.
-
Select the Branch
Choose the branch you want to deploy (typically
main). -
Configure Traffic Type
Select HTTP as the traffic type for Ember.js (a web framework serving HTML/assets).
-
Set the Internal Port
Set the internal port to
4200– this is the port where the Ember development/production server listens. -
Add Environment Variables (Optional)
Add any environment variables your Ember.js app requires:
NODE_ENV=productionAPI_URL=https://api.example.comSITE_NAME=My Ember AppIf you need to customize the Nixpacks build or start command, use these environment variables:
BUILD_COMMAND: Override the default build command (e.g.,npm run build)START_COMMAND: Override the default start command (e.g.,ember serve --environment=production)
-
Configure Compute Resources
Select your region, compute size, and number of instances based on expected traffic.
-
Deploy
Click “Create” to start the deployment. Nixpacks will automatically build and deploy your Ember.js app. Your app will be available at a URL like
https://example-app.klutch.sh.
Deploying with Docker
For more control over your deployment environment, you can use a Dockerfile. Klutch.sh automatically detects a Dockerfile in your repository root and uses it for deployment.
Creating a Dockerfile for Ember.js
Create a Dockerfile in the root of your Ember.js project:
# === Build stage ===FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g ember-cli
COPY package*.json ./RUN npm install
COPY . .RUN npm run build
# === Runtime stage ===FROM node:18-alpine
WORKDIR /app
RUN npm install -g http-server
COPY --from=builder /app/dist ./dist
ENV PORT=4200EXPOSE 4200
CMD ["http-server", "dist", "-p", "4200", "--gzip"]Alternative Dockerfile for Nginx
For a lighter-weight deployment using Nginx:
# === Build stage ===FROM node:18-alpine AS builder
WORKDIR /app
RUN npm install -g ember-cli
COPY package*.json ./RUN npm install
COPY . .RUN npm run build
# === Runtime stage (Nginx) ===FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# Configure Nginx for SPA routingRUN echo 'server { \ listen 80; \ location / { \ root /usr/share/nginx/html; \ try_files $uri $uri/ /index.html; \ } \}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Dockerfile Notes
- Builder stage: Installs Ember CLI and builds your Ember app for production.
- Runtime stage: Uses either http-server or Nginx to serve your built Ember application.
- Port: The
PORTenvironment variable is set to4200for http-server or80for Nginx. - Multi-stage build: Reduces final image size by excluding build tools and dev dependencies from the runtime container.
Steps to Deploy with Docker
-
Create a Dockerfile
Add the Dockerfile (shown above) to the root of your Ember.js repository.
-
Test Locally (Optional)
Build and test the Docker image locally:
Terminal window docker build -t ember-app:latest .docker run -p 4200:4200 ember-app:latestVisit http://localhost:4200 to verify.
-
Push to GitHub
Commit and push the Dockerfile and your code:
Terminal window git add Dockerfilegit commit -m "Add Dockerfile for production deployment"git push origin main -
Create an App in Klutch.sh
Go to klutch.sh/app, navigate to “Create App”, and select your repository.
-
Configure the App
- Traffic Type: Select HTTP
- Internal Port: Set to
4200(or80if using the Nginx Dockerfile) - Environment Variables: Add any required runtime variables
-
Deploy
Klutch.sh automatically detects the Dockerfile and uses it to build and deploy your app. Your app will be available at
https://example-app.klutch.sh.
Environment Variables
Define all environment variables in the Klutch.sh dashboard. Here’s a recommended set for production:
NODE_ENV=productionAPI_URL=https://api.example.comSITE_NAME=My Ember AppANALYTICS_ID=your-analytics-idAUTH_URL=https://auth.example.comAccessing Environment Variables in Ember.js
Access environment variables in your config/environment.js:
ENV.APP.apiURL = process.env.API_URL || 'http://localhost:3000/api';ENV.APP.siteName = process.env.SITE_NAME || 'My Ember App';In components and services, inject the config:
import config from 'my-ember-app/config/environment';
export default class MyService extends Service { get apiURL() { return config.APP.apiURL; }}Persistent Storage
If your Ember.js application needs to store files or user-generated content, you can use persistent volumes in Klutch.sh.
Adding Persistent Volumes
- In the Klutch.sh dashboard, go to your app’s Volumes section.
- Click Add Volume.
- Set the mount path (e.g.,
/data,/uploads, or/var/www/uploads). - Set the size (e.g.,
1 GiB,10 GiB). - Save and redeploy your app.
Example: Handling File Uploads with a Backend API
import Service from '@ember/service';
export default class UploadService extends Service { async uploadFile(file) { const formData = new FormData(); formData.append('file', file);
try { const response = await fetch('/api/upload', { method: 'POST', body: formData, headers: { 'Authorization': `Bearer ${this.getToken()}` } });
if (!response.ok) { throw new Error('Upload failed'); }
return await response.json(); } catch (error) { console.error('Error uploading file:', error); throw error; } }
getToken() { return localStorage.getItem('authToken') || ''; }}Security Best Practices
1. HTTPS/SSL Enforcement
Klutch.sh automatically provides HTTPS for all deployed apps. Configure your Ember app to enforce HTTPS in production.
2. Content Security Policy
Set CSP headers in your backend or configure in Ember:
if (environment === 'production') { ENV.APP.csp = { 'default-src': ["'self'"], 'script-src': ["'self'", "'unsafe-inline'"], 'style-src': ["'self'", "'unsafe-inline'"], 'img-src': ["'self'", 'data:', 'https:'], 'connect-src': ["'self'", 'https://api.example.com'] };}3. Authentication and Authorization
Implement secure authentication patterns:
import Service from '@ember/service';import { service } from '@ember/service';
export default class AuthService extends Service { @service router;
async login(email, password) { try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) });
if (!response.ok) { throw new Error('Login failed'); }
const { token, user } = await response.json(); localStorage.setItem('authToken', token); localStorage.setItem('user', JSON.stringify(user));
return user; } catch (error) { console.error('Authentication error:', error); throw error; } }
async logout() { localStorage.removeItem('authToken'); localStorage.removeItem('user'); this.router.transitionTo('login'); }
get isAuthenticated() { return !!localStorage.getItem('authToken'); }}4. Input Validation
Validate all user inputs:
import Service from '@ember/service';
export default class ValidatorService extends Service { validateEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); }
validatePassword(password) { return password.length >= 8; }
validateArticle(article) { const errors = [];
if (!article.title || article.title.trim().length === 0) { errors.push('Title is required'); }
if (!article.content || article.content.trim().length === 0) { errors.push('Content is required'); }
if (article.title && article.title.length > 200) { errors.push('Title must be less than 200 characters'); }
return { isValid: errors.length === 0, errors }; }}5. Environment Variable Protection
Never commit sensitive data to version control:
API_KEY=your-secret-keyJWT_SECRET=your-jwt-secretDATABASE_URL=postgresql://...6. Dependency Security
Regularly audit and update dependencies:
npm auditnpm audit fixnpm update7. XSS Prevention
Use Ember’s built-in XSS protection:
{{! Always use triple-curlies only for trusted HTML }}{{escaped-value}}
{{! Use safe-string only for your own content }}{{{trusted-html}}}Monitoring and Logging
Health Check Endpoint
Implement a health check endpoint in your backend:
// Backend example (Node.js/Express)app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime() });});Client-Side Error Handling
Implement error handling in your Ember app:
import Service from '@ember/service';import { service } from '@ember/service';
export default class ErrorHandlerService extends Service { @service router;
handleError(error) { console.error('Application error:', error);
if (error.status === 401) { // Handle authentication error this.router.transitionTo('login'); } else if (error.status === 403) { // Handle authorization error this.router.transitionTo('forbidden'); } else if (error.status === 404) { // Handle not found this.router.transitionTo('not-found'); } else { // Handle generic error this.router.transitionTo('error'); } }}Structured Logging
Implement logging for production monitoring:
import Service from '@ember/service';
export default class LoggerService extends Service { log(level, message, data = {}) { const entry = { timestamp: new Date().toISOString(), level, message, data };
if (process.env.NODE_ENV === 'production') { // Send to logging service fetch('/api/logs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(entry) }).catch(err => console.error('Failed to log:', err)); } else { console.log(JSON.stringify(entry)); } }
info(message, data) { this.log('info', message, data); }
warn(message, data) { this.log('warn', message, data); }
error(message, data) { this.log('error', message, data); }}Custom Domains
To use a custom domain with your Klutch.sh-deployed Ember.js app:
1. Add the Domain in Klutch.sh
In the Klutch.sh dashboard, go to your app’s settings and add your custom domain (e.g., app.example.com).
2. Update Your DNS Provider
Update your DNS records with the CNAME provided by Klutch.sh:
CNAME: app.example.com → example-app.klutch.sh3. Update Your App Configuration
Update your config/environment.js with the correct URL:
ENV.rootURL = '/';ENV.APP.apiURL = process.env.API_URL || 'https://api.example.com';4. Wait for DNS Propagation
DNS changes can take up to 48 hours to propagate. Use tools to verify:
nslookup app.example.com# ordig app.example.comOnce propagated, your Ember.js app will be accessible at your custom domain with automatic HTTPS.
Troubleshooting
Issue 1: Build Fails with “Out of Memory”
Error: JavaScript heap out of memory during build
Solutions:
- Increase Node memory:
NODE_OPTIONS=--max-old-space-size=4096 npm run build - Set
BUILD_COMMANDin Klutch.sh:NODE_OPTIONS=--max-old-space-size=4096 npm run build - Review dependencies for unnecessary packages
- Check Klutch.sh build logs for detailed error messages
Issue 2: App Shows Blank Page or 404 Errors
Error: App loads but shows no content or routes return 404
Solutions:
- Verify
config/environment.jshas correctrootURLfor your deployment path - Ensure all routes are properly defined in
app/router.js - Check browser console for JavaScript errors
- Verify your Ember version is compatible with your add-ons
Issue 3: API Requests Fail with CORS Error
Error: CORS policy: no 'Access-Control-Allow-Origin' header errors
Solutions:
- Configure
API_URLenvironment variable to point to your backend - Ensure backend CORS is configured to accept requests from your domain
- Use a CORS proxy if backend doesn’t support CORS
- Verify API endpoint URLs match your model/service configuration
Issue 4: Assets Not Loading (404 Errors)
Error: CSS, images, or JavaScript files return 404
Solutions:
- Verify all asset paths are correct in
app/styles/andapp/templates/ - Ensure
public/directory contains all static assets - Check that asset imports use correct paths
- Rebuild and verify
dist/directory is properly generated
Issue 5: Authentication Token Not Persisting
Error: Users get logged out on page refresh or navigation
Solutions:
- Verify localStorage is being used to persist tokens
- Check that authentication service initializes on app boot
- Ensure
beforeModelhooks restore authentication state - Verify token is sent with API requests
Best Practices
1. Use Computed Properties Effectively
import { computed } from '@ember/object';
export default class ArticleModel extends Model { @attr title; @attr content; @attr author;
@computed('content') get contentLength() { return this.content?.length || 0; }
@computed('contentLength') get readingTime() { return Math.ceil(this.contentLength / 200); // 200 words per minute }}2. Implement Loading States
import Controller from '@ember/controller';import { tracked } from '@glimmer/tracking';
export default class ArticlesController extends Controller { @tracked isLoading = false;
async loadArticles() { this.isLoading = true; try { await this.store.findAll('article'); } finally { this.isLoading = false; } }}3. Use Decorators for Cleaner Code
import { action } from '@ember/object';import { service } from '@ember/service';
export default class MyComponent extends Component { @service api; @service router; @tracked articles = [];
@action async saveArticle(article) { try { await this.api.createArticle(article); this.router.transitionTo('articles'); } catch (error) { console.error('Error saving:', error); } }}4. Implement Proper Error Boundaries
Use try-catch in critical operations and handle errors gracefully.
5. Keep Components Small and Focused
Break down complex components into smaller, reusable components with single responsibilities.
6. Use Ember Data Correctly
Leverage Ember Data’s relationships and querying capabilities for cleaner code.
7. Implement Lazy Loading
Load large datasets incrementally rather than all at once.
8. Use Ember Concurrency for Async Operations
Manage concurrent async operations with proper cancellation and error handling.
9. Test Your Code
Write unit and integration tests for components, services, and routes.
10. Keep Dependencies Updated
Regularly update Ember.js and add-ons to get security patches and improvements.
Verifying Your Deployment
After deployment completes:
- Check the App URL: Visit your app at
https://example-app.klutch.shor your custom domain. - Verify Routes Work: Navigate through different routes to ensure routing works.
- Test Components: Verify all components render and interactive features work.
- Check API Integration: Test that API calls work correctly.
- Check Performance: Use Google PageSpeed Insights to verify performance.
- Review Browser Console: Open F12 and verify no errors are present.
- Review Logs: Check the Klutch.sh dashboard logs for any runtime errors.
If your app doesn’t work as expected, review the troubleshooting section and check the logs in the Klutch.sh dashboard.
External Resources
- Official Ember.js Guides
- Ember.js API Documentation
- Ember Data Guide
- Klutch.sh Official Website
- Node.js Documentation
- Handlebars Templates
- Web Security Documentation
Deploying an Ember.js application to Klutch.sh is straightforward with Nixpacks for automatic deployment or Docker for fine-grained control. By following this guide, you’ve learned how to scaffold an Ember project, create components, services, and routes, configure environment variables, implement security best practices, set up monitoring, and troubleshoot common issues. Your Ember.js application is now running on Klutch.sh’s global infrastructure with automatic HTTPS, custom domain support, and production-grade performance. For additional help or questions, consult the official Ember.js documentation or contact Klutch.sh support.