This page shows a template production deployment on an Ubuntu server. The frontend runs as a Next.js service on localhost, the backend runs as a FastAPI service through Gunicorn and Uvicorn workers, Celery worker and beat run through systemd, Redis stays local, and Nginx is the only public entry point.
Overview
- Run the Next.js frontend on 127.0.0.1:3000.
- Run the FastAPI backend on 127.0.0.1:8000.
- Run Redis locally on 127.0.0.1:6379.
- Run Celery worker and Celery beat with systemd.
- Expose only Nginx on ports 80 and 443.
- Route the main domain to the frontend.
- Route the API subdomain to the backend.
- Use Cloudflare Origin Certificate when Cloudflare proxies the domain.
Template variables
Use these placeholders throughout the guide. Replace them with the real values for the target project.
| Placeholder | Meaning |
|---|---|
| example-app | Short project or service name |
| example.com | Main frontend domain |
| www.example.com | Frontend www domain |
| api.example.com | Backend API domain |
| /home/ubuntu/example_backend | Backend repository path |
| /home/ubuntu/example_frontend | Frontend repository path |
| /home/ubuntu/example_venv | Python virtual environment path |
| example-backend | Backend systemd service name |
| example-frontend | Frontend systemd service name |
| example-celery-worker | Celery worker systemd service name |
| example-celery-beat | Celery beat systemd service name |
| main:app | FastAPI application import path |
| celery_app:app | Celery application import path |
Repository and runtime layout
Keep the frontend and backend in separate folders. Keep the Python virtual environment outside the backend repository so it is not mixed with application source code.
| Component | Template path |
|---|---|
| Backend | /home/ubuntu/example_backend |
| Frontend | /home/ubuntu/example_frontend |
| Python virtual environment | /home/ubuntu/example_venv |
source /home/ubuntu/example_venv/bin/activatePorts and public exposure
Bind application services to localhost. Nginx is the only public service. This keeps Node, FastAPI, Redis, and Celery internals away from direct internet access.
| Service | Bind address |
|---|---|
| Frontend | 127.0.0.1:3000 |
| Backend | 127.0.0.1:8000 |
| Redis | 127.0.0.1:6379 |
| Nginx HTTP | 0.0.0.0:80 |
| Nginx HTTPS | 0.0.0.0:443 |
Domain routing
Use the root domain and www domain for the frontend. Use an api subdomain for the backend.
| Domain | Target |
|---|---|
| example.com | Frontend on 127.0.0.1:3000 |
| www.example.com | Frontend on 127.0.0.1:3000 |
| api.example.com | Backend on 127.0.0.1:8000 |
In Cloudflare DNS, create A records that point the root domain, www, and api to the server public IP. When using Cloudflare Origin Certificate, keep these records proxied and set SSL/TLS mode to Full strict.
Install OS packages
Install Nginx and Redis on the server. Certbot is not required when the project uses Cloudflare Origin Certificate files.
sudo apt-get update
sudo apt-get install -y nginx redis-serverBackend environment file
Keep the backend runtime environment file inside the backend project folder. Do not commit this file to the repository.
/home/ubuntu/example_backend/.env- CELERY_BROKER_URL is required when Celery uses Redis.
- CELERY_RESULT_BACKEND is optional unless the project stores task results.
- Add database URLs, application secrets, API keys, and other runtime settings required by the backend.
- The backend service and Celery services should read the same backend environment file.
Frontend environment file
Keep the Next.js production environment file inside the frontend project folder. NEXT_PUBLIC variables are baked into the build, so changing them requires a rebuild.
/home/ubuntu/example_frontend/.env.production- Set NEXT_PUBLIC_API_URL to https://api.example.com.
- Rebuild the frontend after changing NEXT_PUBLIC variables.
- Restart the frontend service after rebuilding.
Redis setup
Redis runs locally as the Celery broker and optional result backend. Keep Redis bound to localhost and do not expose port 6379 publicly.
sudo systemctl enable redis-server
sudo systemctl start redis-server
sudo systemctl status redis-server --no-pager
redis-cli pingThe expected Redis ping response is PONG.
Backend systemd service
Create a systemd service for the FastAPI backend. Gunicorn starts Uvicorn workers and binds the app to localhost on port 8000.
sudo nano /etc/systemd/system/example-backend.servicePaste the following and adjust main:app if the FastAPI entrypoint is different.
[Unit]
Description=Example FastAPI Backend
After=network.target
[Service]
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/example_backend
EnvironmentFile=/home/ubuntu/example_backend/.env
ExecStart=/home/ubuntu/example_venv/bin/gunicorn -k uvicorn.workers.UvicornWorker -w 2 -b 127.0.0.1:8000 main:app
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable example-backend
sudo systemctl start example-backend
sudo systemctl status example-backend --no-pager
journalctl -u example-backend -n 200 --no-pagerCelery worker systemd service
Create a separate service for the Celery worker. The example assumes the Celery instance is app inside celery_app.py, so the target is celery_app:app.
sudo nano /etc/systemd/system/example-celery-worker.service[Unit]
Description=Example Celery Worker
After=network.target redis-server.service
Requires=redis-server.service
[Service]
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/example_backend
EnvironmentFile=/home/ubuntu/example_backend/.env
ExecStart=/home/ubuntu/example_venv/bin/celery -A celery_app:app worker -l info --concurrency=2
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable example-celery-worker
sudo systemctl start example-celery-worker
sudo systemctl status example-celery-worker --no-pager
journalctl -u example-celery-worker -n 200 --no-pagerCelery beat systemd service
Create a separate service for Celery beat when the application has scheduled jobs. Use an empty pidfile argument to avoid stale pidfile issues in simple deployments.
sudo nano /etc/systemd/system/example-celery-beat.service[Unit]
Description=Example Celery Beat
After=network.target redis-server.service
Requires=redis-server.service
[Service]
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/example_backend
EnvironmentFile=/home/ubuntu/example_backend/.env
ExecStart=/home/ubuntu/example_venv/bin/celery -A celery_app:app beat -l info --pidfile=
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable example-celery-beat
sudo systemctl start example-celery-beat
sudo systemctl status example-celery-beat --no-pager
journalctl -u example-celery-beat -n 200 --no-pagerOptional Celery beat schedule path
For deployments that need a persistent beat schedule file outside the repository, create a writable runtime folder and pass the schedule path in ExecStart.
sudo mkdir -p /var/lib/example-app
sudo chown ubuntu:ubuntu /var/lib/example-appExecStart=/home/ubuntu/example_venv/bin/celery -A celery_app:app beat -l info --pidfile= --schedule=/var/lib/example-app/celerybeat-scheduleFrontend build
Build the Next.js frontend from the frontend project folder. Production environment variables must already be correct before building.
cd /home/ubuntu/example_frontend
npm ci
npm run buildFrontend systemd service
Create a systemd service for the Next.js production server. The service runs npm start on localhost port 3000.
sudo nano /etc/systemd/system/example-frontend.service[Unit]
Description=Example Next.js Frontend
After=network.target
[Service]
User=ubuntu
Group=ubuntu
WorkingDirectory=/home/ubuntu/example_frontend
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/npm run start
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable example-frontend
sudo systemctl start example-frontend
sudo systemctl status example-frontend --no-pager
journalctl -u example-frontend -n 200 --no-pagerRestart frontend after rebuild
When frontend code or NEXT_PUBLIC environment variables change, rebuild first and then restart the frontend service.
cd /home/ubuntu/example_frontend
npm run build
sudo systemctl restart example-frontendNginx site file
Create one Nginx site file for the frontend and backend domains. The HTTP blocks redirect to HTTPS. The HTTPS frontend block proxies to port 3000. The HTTPS API block proxies to port 8000.
sudo nano /etc/nginx/sites-available/example-appserver {
listen 80;
server_name example.com www.example.com api.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example-app/origin-cert.pem;
ssl_certificate_key /etc/nginx/ssl/example-app/origin-key.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/example-app/origin-cert.pem;
ssl_certificate_key /etc/nginx/ssl/example-app/origin-key.pem;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Enable Nginx site
Enable the site by linking it into sites-enabled. Remove the default site if it conflicts with the same domains.
sudo ln -s /etc/nginx/sites-available/example-app /etc/nginx/sites-enabled/example-app
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxCloudflare Origin Certificate files
This certificate pattern assumes Cloudflare proxies traffic and SSL/TLS mode is Full strict. Store the origin certificate and private key on the server and reference them from the Nginx HTTPS blocks.
sudo mkdir -p /etc/nginx/ssl/example-app
sudo nano /etc/nginx/ssl/example-app/origin-cert.pem
sudo nano /etc/nginx/ssl/example-app/origin-key.pemsudo chmod 600 /etc/nginx/ssl/example-app/origin-key.pem
sudo chmod 644 /etc/nginx/ssl/example-app/origin-cert.pem
sudo chown -R root:root /etc/nginx/ssl/example-app
sudo nginx -t
sudo systemctl reload nginxService status commands
Use these commands to inspect all services involved in the deployment.
sudo systemctl status example-backend --no-pager
sudo systemctl status example-frontend --no-pager
sudo systemctl status example-celery-worker --no-pager
sudo systemctl status example-celery-beat --no-pager
sudo systemctl status redis-server --no-pager
sudo systemctl status nginx --no-pagerRestart commands
Restart only the service that changed. Restart Nginx only after Nginx config or certificate changes.
sudo systemctl restart example-backend
sudo systemctl restart example-frontend
sudo systemctl restart example-celery-worker
sudo systemctl restart example-celery-beat
sudo systemctl restart redis-server
sudo systemctl restart nginxLog commands
Use journalctl for systemd service logs. Use follow mode when watching live behavior after a restart.
journalctl -u example-backend -f
journalctl -u example-frontend -f
journalctl -u example-celery-worker -f
journalctl -u example-celery-beat -f
journalctl -u nginx -n 200 --no-pager
journalctl -u redis-server -n 200 --no-pagerLocal health checks
Run local checks from the server first. These confirm the internal services respond before testing public domains.
curl -I http://127.0.0.1:3000
curl -I http://127.0.0.1:8000
redis-cli pingExternal health checks
After local checks pass and Nginx reloads successfully, test the public HTTPS domains.
curl -I https://example.com
curl -I https://www.example.com
curl -I https://api.example.comExpected final state
- Nginx is the only public service on ports 80 and 443.
- The frontend runs locally on 127.0.0.1:3000.
- The backend runs locally on 127.0.0.1:8000.
- Redis runs locally and responds with PONG.
- Celery worker and Celery beat run as systemd services.
- The main domain serves the frontend.
- The API subdomain serves the backend.
- Cloudflare proxies the domains and uses Full strict SSL mode.
Common mistakes
- Exposing the backend or Redis publicly instead of binding them to localhost.
- Changing NEXT_PUBLIC frontend variables without rebuilding the frontend.
- Forgetting to run sudo systemctl daemon-reload after editing service files.
- Using the wrong FastAPI app path in the Gunicorn ExecStart command.
- Using the wrong Celery app target. If the file is celery_app.py and the instance is app, use celery_app:app.
- Reloading Nginx without first running sudo nginx -t.
- Using Cloudflare Origin Certificate without Cloudflare proxy and Full strict mode.
- Putting private keys or environment files into the repository.