systemd and Nginx Fullstack Deployment
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
Section titled “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
Section titled “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
Section titled “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
Section titled “Ports 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
Section titled “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
Section titled “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 updatesudo apt-get install -y nginx redis-serverBackend environment file
Section titled “Backend 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
Section titled “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
Section titled “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-serversudo systemctl start redis-server
sudo systemctl status redis-server --no-pagerredis-cli pingThe expected Redis ping response is PONG.
Backend systemd service
Section titled “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 BackendAfter=network.target
[Service]User=ubuntuGroup=ubuntuWorkingDirectory=/home/ubuntu/example_backendEnvironmentFile=/home/ubuntu/example_backend/.envExecStart=/home/ubuntu/example_venv/bin/gunicorn -k uvicorn.workers.UvicornWorker -w 2 -b 127.0.0.1:8000 main:appRestart=alwaysRestartSec=5
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable example-backendsudo systemctl start example-backend
sudo systemctl status example-backend --no-pagerjournalctl -u example-backend -n 200 --no-pagerCelery worker systemd service
Section titled “Celery 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 WorkerAfter=network.target redis-server.serviceRequires=redis-server.service
[Service]User=ubuntuGroup=ubuntuWorkingDirectory=/home/ubuntu/example_backendEnvironmentFile=/home/ubuntu/example_backend/.envExecStart=/home/ubuntu/example_venv/bin/celery -A celery_app:app worker -l info --concurrency=2Restart=alwaysRestartSec=5
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable example-celery-workersudo systemctl start example-celery-worker
sudo systemctl status example-celery-worker --no-pagerjournalctl -u example-celery-worker -n 200 --no-pagerCelery beat systemd service
Section titled “Celery 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 BeatAfter=network.target redis-server.serviceRequires=redis-server.service
[Service]User=ubuntuGroup=ubuntuWorkingDirectory=/home/ubuntu/example_backendEnvironmentFile=/home/ubuntu/example_backend/.envExecStart=/home/ubuntu/example_venv/bin/celery -A celery_app:app beat -l info --pidfile=Restart=alwaysRestartSec=5
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable example-celery-beatsudo systemctl start example-celery-beat
sudo systemctl status example-celery-beat --no-pagerjournalctl -u example-celery-beat -n 200 --no-pagerOptional Celery beat schedule path
Section titled “Optional 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-appsudo 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
Section titled “Frontend build”Build the Next.js frontend from the frontend project folder. Production environment variables must already be correct before building.
cd /home/ubuntu/example_frontendnpm cinpm run buildFrontend systemd service
Section titled “Frontend 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 FrontendAfter=network.target
[Service]User=ubuntuGroup=ubuntuWorkingDirectory=/home/ubuntu/example_frontendEnvironment=NODE_ENV=productionEnvironment=PORT=3000ExecStart=/usr/bin/npm run startRestart=alwaysRestartSec=5
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable example-frontendsudo systemctl start example-frontend
sudo systemctl status example-frontend --no-pagerjournalctl -u example-frontend -n 200 --no-pagerRestart frontend after rebuild
Section titled “Restart frontend after rebuild”When frontend code or NEXT_PUBLIC environment variables change, rebuild first and then restart the frontend service.
cd /home/ubuntu/example_frontendnpm run buildsudo systemctl restart example-frontendNginx site file
Section titled “Nginx 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
Section titled “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-appsudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -tsudo systemctl reload nginxCloudflare Origin Certificate files
Section titled “Cloudflare 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.pemsudo nano /etc/nginx/ssl/example-app/origin-key.pemsudo chmod 600 /etc/nginx/ssl/example-app/origin-key.pemsudo chmod 644 /etc/nginx/ssl/example-app/origin-cert.pemsudo chown -R root:root /etc/nginx/ssl/example-app
sudo nginx -tsudo systemctl reload nginxService status commands
Section titled “Service status commands”Use these commands to inspect all services involved in the deployment.
sudo systemctl status example-backend --no-pagersudo systemctl status example-frontend --no-pagersudo systemctl status example-celery-worker --no-pagersudo systemctl status example-celery-beat --no-pagersudo systemctl status redis-server --no-pagersudo systemctl status nginx --no-pagerRestart commands
Section titled “Restart commands”Restart only the service that changed. Restart Nginx only after Nginx config or certificate changes.
sudo systemctl restart example-backendsudo systemctl restart example-frontendsudo systemctl restart example-celery-workersudo systemctl restart example-celery-beatsudo systemctl restart redis-serversudo systemctl restart nginxLog commands
Section titled “Log commands”Use journalctl for systemd service logs. Use follow mode when watching live behavior after a restart.
journalctl -u example-backend -fjournalctl -u example-frontend -fjournalctl -u example-celery-worker -fjournalctl -u example-celery-beat -fjournalctl -u nginx -n 200 --no-pagerjournalctl -u redis-server -n 200 --no-pagerLocal health checks
Section titled “Local 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:3000curl -I http://127.0.0.1:8000redis-cli pingExternal health checks
Section titled “External health checks”After local checks pass and Nginx reloads successfully, test the public HTTPS domains.
curl -I https://example.comcurl -I https://www.example.comcurl -I https://api.example.comExpected final state
Section titled “Expected 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
Section titled “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.