Ctrl K

systemd and Nginx Fullstack Deployment

Deploy a Next.js frontend and FastAPI backend on Ubuntu with systemd, Redis, Celery, Nginx, and Cloudflare Origin Certificate SSL.

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.

PlaceholderMeaning
example-appShort project or service name
example.comMain frontend domain
www.example.comFrontend www domain
api.example.comBackend API domain
/home/ubuntu/example_backendBackend repository path
/home/ubuntu/example_frontendFrontend repository path
/home/ubuntu/example_venvPython virtual environment path
example-backendBackend systemd service name
example-frontendFrontend systemd service name
example-celery-workerCelery worker systemd service name
example-celery-beatCelery beat systemd service name
main:appFastAPI application import path
celery_app:appCelery 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.

ComponentTemplate path
Backend/home/ubuntu/example_backend
Frontend/home/ubuntu/example_frontend
Python virtual environment/home/ubuntu/example_venv
source /home/ubuntu/example_venv/bin/activate

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.

ServiceBind address
Frontend127.0.0.1:3000
Backend127.0.0.1:8000
Redis127.0.0.1:6379
Nginx HTTP0.0.0.0:80
Nginx HTTPS0.0.0.0:443

Domain routing

Use the root domain and www domain for the frontend. Use an api subdomain for the backend.

DomainTarget
example.comFrontend on 127.0.0.1:3000
www.example.comFrontend on 127.0.0.1:3000
api.example.comBackend 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-server

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

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 ping

The 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.service

Paste 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.target
sudo 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-pager

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 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.target
sudo 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-pager

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 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.target
sudo 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-pager

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-app
sudo chown ubuntu:ubuntu /var/lib/example-app
ExecStart=/home/ubuntu/example_venv/bin/celery -A celery_app:app beat -l info --pidfile= --schedule=/var/lib/example-app/celerybeat-schedule

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_frontend
npm ci
npm run build

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 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.target
sudo 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-pager

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_frontend
npm run build
sudo systemctl restart example-frontend

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-app
server {
    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 nginx

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.pem
sudo nano /etc/nginx/ssl/example-app/origin-key.pem
sudo 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 nginx

Service 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-pager

Restart 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 nginx

Log 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-pager

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:3000
curl -I http://127.0.0.1:8000
redis-cli ping

External 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.com

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

  • 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.