FlatRun uses Nginx as a reverse proxy to route incoming traffic to your deployments based on domain names. This enables you to run multiple applications on a single server, each with its own domain.
How It Works
- A request comes in for
blog.example.com - Nginx receives the request on port 80/443
- Nginx checks its virtual host configuration
- The request is forwarded to the appropriate container
- The container's response is sent back to the client
Nginx Configuration
Configure Nginx in your agent config:
nginx:
enabled: true
image: nginx:alpine
container_name: nginx
config_path: ""
reload_command: "nginx -s reload"
external: false
container_webroot_path: /usr/share/nginx/html | Option | Description |
|---|---|
enabled | Enable Nginx integration |
image | Docker image for Nginx |
container_name | Name of the Nginx container |
config_path | Path to config directory (auto-detected if empty) |
reload_command | Command to reload Nginx config |
external | Use external Nginx (not managed by FlatRun) |
Directory Structure
Nginx configuration is stored in the deployments path:
/var/flatrun/deployments/
└── nginx/
├── docker-compose.yml # Nginx container definition
├── conf.d/ # Virtual host configs
│ ├── blog.example.com.conf
│ └── api.example.com.conf
├── certs/ # SSL certificates
│ └── live/
│ └── example.com/
└── html/ # Static files, certbot webroot Virtual Host Configuration
FlatRun automatically generates virtual host configurations when you set up proxy for a deployment. A typical config looks like:
server {
listen 80;
server_name blog.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name blog.example.com;
ssl_certificate /etc/nginx/certs/live/blog.example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/live/blog.example.com/privkey.pem;
location / {
proxy_pass http://my-blog-wordpress:80;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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_cache_bypass $http_upgrade;
}
} Setting Up Proxy for a Deployment
Via UI
- Go to Deployments and select your deployment
- Go to the Overview tab
- In the Domain & SSL section, click "Set Up Proxy"
- Enter the domain name
- Select the container port
- Optionally enable SSL
- Click "Save"
Via API
curl -X POST "http://localhost:8090/api/proxy/setup/my-blog" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"domain": "blog.example.com",
"port": 80,
"ssl": true
}' Via service.yml
name: My Blog
networking:
expose: true
domain: blog.example.com
port: 80
ssl:
enabled: true
auto_renew: true Proxy Network
For Nginx to communicate with your deployment containers, they must be on the same Docker network. FlatRun uses a proxy network for this purpose.
# Create the proxy network (if not exists)
docker network create proxy
# Deployments should connect to this network
services:
web:
image: myapp
networks:
- default
- proxy
networks:
proxy:
external: true External Nginx
If you have an existing Nginx installation (on the host or another container), set external: true:
nginx:
enabled: true
external: true
config_path: /etc/nginx/conf.d
reload_command: "systemctl reload nginx" FlatRun will generate config files but won't manage the Nginx container.
Load Balancing
For deployments with multiple replicas, configure an upstream block:
upstream myapp_backend {
server myapp_1:3000;
server myapp_2:3000;
server myapp_3:3000;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://myapp_backend;
}
} WebSocket Support
Generated virtual hosts support WebSockets out of the box. The
Connection: upgrade header is sent only for requests that actually ask to
upgrade, so ordinary HTTP requests on the same domain keep upstream keep-alive intact.
Idle Timeout
The proxy read and send timeout defaults to 60 seconds. A WebSocket that
sits idle (no frames) for longer than that is closed by Nginx, which shows up as reconnect
loops on quiet connections. For deployments that hold long-lived sockets, raise the
per-domain timeout with proxy_timeout (in seconds):
domains:
- domain: app.example.com
service: app
container_port: 8080
proxy_timeout: 3600
ssl:
enabled: true
auto_cert: true proxy_timeout sets both the read and send timeouts; the connect timeout stays
at 60 seconds. Leave it unset to keep the 60-second default.
Per-domain settings
Each domain carries its own serving settings in the project's service.yml. Set them from the domain form, or change one at a time with PATCH /api/projects/:name/domains/:domainId. A domain that sets none keeps the stock proxy behaviour.
| Field | Description |
|---|---|
client_max_body_size | Largest request the domain accepts, such as 512k, 64m or 1g (at most 10g; 0 for no limit). Larger requests get HTTP 413 before they reach the app. |
upload_limit_page | Answer a refused upload with a page stating the limit, or application/problem+json for API clients, instead of nginx's plain 413. Off by default. A 413 sent by the app itself is never changed. |
proxy_buffering | Set to false to send the app's response as it is written, for server-sent events and other streaming responses. |
response_headers | Headers added to every response. A header named like a default security header replaces it; the others stay. Values cannot contain $. |
canonical_redirect | With primary, visitors on the domain's aliases get a permanent redirect to the primary domain. An alias needs its own certificate to redirect over HTTPS. |
basic_auth | Ask for a username and password before the request reaches the app. Passwords are stored only as hashes; send a user with only its username to keep its password. |
allow_ips | Only these addresses and CIDR ranges reach the domain; everyone else gets 403. Addresses the project's security settings block stay blocked. |
domains:
- id: media
domain: media.example.com
service: web
container_port: 80
client_max_body_size: 64m
upload_limit_page: true
proxy_buffering: false
response_headers:
Cache-Control: no-store
allow_ips:
- 203.0.113.0/24 Sizing the upload limit
The limit covers the whole request, not each file. A form upload wraps every file in boundaries and part headers and sends other fields with it, so the request is always somewhat larger than the file. Set the limit about 10% above the app's own per-file limit: for a 100 MB file limit, use 110m. PHP apps also enforce upload_max_filesize and post_max_size, so raise those too.
The default for domains without their own limit is set under Settings, Infrastructure, or with PUT /api/config/nginx.client_max_body_size. Changing it rewrites every site's proxy configuration. Leave it empty to keep the value in nginx.conf, which is nginx's 1m unless it is raised there.
Refused uploads
Every request nginx refuses for its size is counted per domain and listed at GET /api/projects/:name/upload-rejections. The project page shows them with the limit, and a proxy / request_too_large notification goes out at most once every 15 minutes per domain.
Rejected changes
Every proxy change is checked with nginx -t before nginx reloads. If the check or the reload fails, the previous configuration stays in place and the API returns nginx's error.
Custom Nginx Configuration
You can add custom configuration by creating files in the conf.d directory. FlatRun won't overwrite files it didn't create.
# Custom rate limiting
# /var/flatrun/deployments/nginx/conf.d/rate-limit.conf
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
# Custom headers
# /var/flatrun/deployments/nginx/conf.d/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always; Troubleshooting
502 Bad Gateway
- Container is not running
- Container not on the proxy network
- Wrong port in proxy configuration
- Container name mismatch
Config Not Reloading
- Check Nginx syntax:
docker exec nginx nginx -t - Manually reload:
docker exec nginx nginx -s reload - Check FlatRun logs for errors
Route Saved but Returns Nothing
When you set up a domain whose target service or port has nothing listening, FlatRun saves the route and reloads, but reports a warning that the target is unreachable. Check the setup response (or the agent logs) for that warning, then confirm the service name and container port match a running container. The route is not blocked, so a target that comes up later starts serving without re-saving.
Domain Not Resolving
- Ensure DNS points to your server
- Check that port 80/443 is open in firewall
- Verify Nginx is listening on correct ports