Running your project
Python apps, Java apps and reverse proxies
What this page is for: three more kinds of website beside the static website, the Node.js app, the PHP website and WordPress:
- a Python app — Django, Flask, FastAPI, or any WSGI or ASGI app;
- a Java app — Spring Boot, Quarkus, Micronaut, or anything that runs from a JAR;
- a reverse proxy — your domain, with HTTPS, in front of an app that runs somewhere else: on another port of this server, or on another server.
Each is a project in the Projects list with its own menu, its own Linux account and its own folder, exactly like the other websites (Websites and apps). Nothing about your 6amMart projects or your other websites changes when you add one.
You need
- For a Python or Java app: its code — a folder, a ZIP or a git repository. A Java app can also be uploaded as a ready
.jar. - For a reverse proxy: the address it sends visitors to, like
http://127.0.0.1:3000orhttps://app.example.com. - A web address for it (optional when you create it — add it later on Domain & SSL).
- Nothing to install first. What a Python or Java app needs is installed by itself the first time it is needed (see Software).
Create one
Projects → Create a new project, then Python app, Java app or Reverse proxy. A Python or Java app asks for a name, an optional web address and whether it needs a MariaDB database. A reverse proxy asks for a name, the optional web address, and two things of its own:
- Send visitors to — the address of the app behind it.
- Host header — the name the app behind is told it was asked for (see Host header). Automatic is right almost always.
A Python or Java app opens on Deploys once it is made; a reverse proxy opens on Domain & SSL, because its address is all it still needs.
Python app
- Deploys — drop the folder or ZIP on the page, or connect git.
- The panel reads the project and fills in how it runs:
- Django (a
manage.py): gunicorn, or uvicorn when the project uses Channels or Daphne and has anasgi.py; the app module (mysite.wsgi:application);python manage.py migrate --noinputas a deploy command;db.sqlite3andmedia/kept between deploys. - Flask: gunicorn, with the app object or the
create_app()factory it finds;instance/kept between deploys. - FastAPI, Starlette, Quart, Litestar and Sanic: uvicorn, with the app object it finds.
- Anything else: the
wsgi.py/asgi.pyit finds, orpython app.py(ormain.py,server.py,run.py) as the start command.
- Every deploy makes a virtual environment of its own for the new version (
.venvinside it) and installsrequirements.txt— or the project itself when there is apyproject.toml— with the server's own Python. gunicorn or uvicorn is added when the project does not list it. - Django:
collectstaticruns on every deploy, and nginx servesSTATIC_URLandMEDIA_URLitself — your app is never asked for a picture. This needsSTATIC_ROOTinsettings.py(STATIC_ROOT = BASE_DIR / "staticfiles"); the deploy log says so when it is missing. - The new version starts beside the live one, must answer on the Health check path, and only then do visitors move over. A version that does not start never goes live; the last three versions stay for a one-click rollback.
What your app is given (in its environment, also for its deploy commands and its scheduled tasks):
PORTandHOST— the panel's. gunicorn and uvicorn are started on127.0.0.1and that port for you. Your own start command must listen there too: a version that listens on any other address is refused and never goes live.SIXPANEL_SITE_HOSTS— this website's addresses, comma-separated, andSIXPANEL_SITE_URL— its main one withhttps://.- With a database:
DATABASE_URL,DB_HOST,DB_PORT,DB_DATABASE,DB_USERNAME,DB_PASSWORDandDB_SOCKET. Nothing to copy. - Your own variables from Environment (any
.envin your files is imported there).
Django behind the panel — three lines in settings.py make it safe and correct behind nginx and HTTPS:
import os
ALLOWED_HOSTS = os.environ.get("SIXPANEL_SITE_HOSTS", "localhost").split(",")
CSRF_TRUSTED_ORIGINS = ["https://" + h for h in ALLOWED_HOSTS]
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")Kept between deploys (Settings): files and folders the app writes that a new version must not lose — uploads, a SQLite file. They live in the website's own data/ folder; each version links to them, and backups carry them. A folder ends in /.
Python versions. The server's own Python is the one its distribution ships (Ubuntu 26.04: 3.14, Ubuntu 24.04: 3.12, Debian 13: 3.13), and it is the default. Other CPython versions (3.11, 3.12, 3.13, 3.15 …) can be installed beside it on Software → Python — see below. A website builds with:
- the version chosen in Settings → Python version (the server's own, or an installed one), or
- with Match the project (the default), the version its project asks for (
requires-pythoninpyproject.toml, or.python-version): the server's own when it fits, else the newest installed one that does. When nothing installed fits, the deploy log says so and names the version to install, and the build uses the server's own.
The version live now is the one its virtual environment was made with: choosing another or installing a new one moves nothing that is running — the next deploy builds with it, and a rollback runs on the version that release was built with. After the server's operating system is upgraded to a new release, deploy once every Python website that uses the server's own Python: its virtual environment was made with the old one.
Packages without a ready wheel (mysqlclient and a few others) need the build tools: Software → Python → Build tools → Install (about 250 MB), then deploy again. PyMySQL needs nothing.
Java app
- Deploys — drop the source folder or ZIP, connect git, or drop a ready
.jar. - The panel reads the project:
pom.xml: built with the project's./mvnw, or with Maven when it has none (-DskipTests package).build.gradle/build.gradle.kts: built with the project's./gradlew(bootJarfor Spring Boot). A Gradle project without its wrapper is refused with the reason — the distribution's own Gradle is too old.- plain
.javafiles: compiled withjavac; the class with amainmethod is run. - a ready
.jar: run as it is. It must name itsMain-Class(every Spring Boot JAR does); a library JAR is refused at once.
- It runs with
java -Xmx… -jar …as the website's own account. The heap is 70% of the website's memory setting; the rest is the JVM's own. - The new version must answer within the start time allowed — 180 seconds unless you change it, because a Java app is slow to start — and only then do visitors move over.
Java versions. The project says which Java it needs (maven.compiler.release, java.version, a Gradle toolchain). The build uses the oldest installed long-term version that is at least that — and when none is installed, the deploy installs one by itself from the distribution. Settings → Java version chooses one for the next build instead. The version live now is the one it was built with: installing a newer Java never moves a running app, and a rollback runs on the version that release was built with.
What your app is given: Spring Boot, Quarkus and Micronaut are told 127.0.0.1 and the port by themselves. Any other server must listen on HOST (127.0.0.1) and PORT. With a database: SPRING_DATASOURCE_URL, SPRING_DATASOURCE_USERNAME, SPRING_DATASOURCE_PASSWORD and the same three as JDBC_DATABASE_URL, JDBC_DATABASE_USERNAME, JDBC_DATABASE_PASSWORD — a Spring Boot app needs no configuration for it. SIXPANEL_SITE_HOSTS and your own variables as above.
Settings has the build command, the JAR to run (target/app.jar), JVM options (-Dspring.profiles.active=prod — not -Xmx, which follows the memory setting), program arguments, deploy commands (a database migration) and the paths kept between deploys.
Reverse proxy
A reverse proxy runs nothing on this server: nginx answers on your domain with HTTPS and sends every request on to the address you gave. It has no files, no database and no deploys — its page is Reverse proxy.
Where visitors go
Send visitors to — http:// or https://, a name or an IP address, a port, and optionally a path (https://app.example.com/base); the visitor's own path and query follow it. Some addresses are refused, with the reason:
- the cloud's metadata address (
169.254.169.254and the names that lead to it — it hands out this server's own cloud credentials); - SixPanel's own port, and nginx's own ports 80 and 443 on this server (visitors would go round in a circle);
- ports that are not web apps (SSH, mail, DNS);
- this website's own domain.
A name is looked up by the panel, every answer is checked against the same list, and nginx is given the addresses — never the name. The name is looked up again every five minutes; a lookup that fails keeps the addresses it had, and one that suddenly answers a refused address takes them away (the website then answers 502 rather than send anybody there). Look the name up again does it at once.
A port of another website on this server (a Node.js, Python or Java app's slot) is followed: those apps move between two ports at every deploy, and the proxy moves with the live one by itself.
Test the target sends one request the way the website sends it, and says the HTTP status, how long it took and whether the certificate checked out.
Host header
- Automatic — the visitor's domain for an IP address (an app on this server), the target's own name for a name (another server, which usually serves several sites and needs its own name).
- The visitor's domain, the target's own name, or a name you give.
How it connects
Websockets (on by default), the connect and answer timeouts, the largest upload, buffering (switch it off for a live stream or server-sent events), and — for an https:// target — whether its certificate is checked (automatic: checked for a name, not for an IP address) and whether the server name is sent (SNI). Refuse requests for .env, .git and similar files stops those before they reach the target.
The app behind receives X-Forwarded-For, X-Forwarded-Proto: https, X-Forwarded-Host and X-Real-IP.
Extra request headers
Headers sent to the target with every request — an API key the target expects, for example. A header whose name says it is a key, a token, a password or a cookie is kept secret (you can tick or untick it): its value is stored readable by root only, is never shown again — here, in the API or to Tia — and is replaced by typing a new one.
Login
Ask every visitor for a user name and password puts a browser login (basic auth) in front of the target. Add users below it; a password is stored only as a hash. With the login on and no user yet, every visitor is refused — it never opens by mistake. The password a visitor types for this login is not passed on to the target.
Short cache
Keeps pages the target allows to be cached for a few seconds and shares them between visitors. A visitor with a cookie or a login always gets a fresh answer, and nothing is stored from one. Overview → Empty the cache empties it.
Web Rules
Redirects, rewrites and response headers work on a reverse proxy as on every website (Web Rules, in its menu), and on Python and Java apps too. A Rewrite on a proxy whose target has a path is sent on after that path: /old served as /new reaches https://app.example.com/base/new. The cost, on such a website only: an encoded slash (%2F) in an address reaches the target as a plain /. A target path that itself holds an encoded character (/my%20app) is always sent addresses exactly as visitors typed them, so a Rewrite does not reach it — use a redirect there.
What it does not have
No Deploys, Database, Environment or Cron jobs — nothing runs here. Nothing of it is in a backup of its own: its settings, users and secret headers are part of the panel's data, which every backup carries. A staging site is not offered: to try another target, add a second reverse proxy.
Software → Python and Java
- Python — the server's own Python, the virtual-environment module every Python website needs (installed by itself the first time one is created), and the optional build tools. Removing is offered only for what the panel installed; Python itself is the system's and stays. Versions lists the server's own and every other version installed, with the websites on each, and the versions that can be added. They come from python-build-standalone (the builds
uvuses), each download checked against that release's published SHA-256 before anything is unpacked — a download that does not match installs nothing — into the panel's runtimes folder (about 100 MB each), on both amd64 and arm64 servers. Remove is offered only for a version no website builds or runs on. - Updates (on Python's and Java's pages, and on Software → Node.js) — each version installed for websites, and whether a newer patch of it exists. Check for updates asks upstream; Update installs it and restarts the running websites on it (a second or two each); Hold keeps a version where it is; Skip this release passes over one release until a newer one comes. A Python patch (3.13.15 → 3.13.16) replaces the version's folder in place, so every website's virtual environment keeps working with no rebuild; if a website does not start on it, the old build is put back. A Java update is the distribution's newer package of that version. The server's own Python follows the distribution's daily security updates and is not updated here.
- Java — the long-term OpenJDK versions this server's distribution offers, which are installed, and which websites build or run on each; Install adds one ahead of time, Remove takes away one that nothing uses. Maven is installed by itself the first time a project with a
pom.xmland no./mvnwneeds it.
Only the owner's login installs or removes software.
What Tia can do
Tia creates all three kinds ("put my app on port 3000 behind shop.example.com" is a reverse proxy with its domain and HTTPS in one go), changes a reverse proxy's settings, tests it, installs or removes Python's build tools, Python versions and Java versions, and checks for and installs their updates. Making a reverse proxy, pointing one somewhere new, adding a secret header or a login user and turning its login off always wait for your approval. A password or a secret header value is never typed to her: she asks for it with a secure field, and she only ever sees ****.
How to check it worked
- Overview — Is it live? reads Yes, the address answers over HTTPS, and a Python or Java app's chip says the Python or the Java it runs on.
- Deploys — the version live now, and its log: what was detected, installed and built.
- Reverse proxy → Test the target answers with the HTTP status.
- Logs → App output — what the app printed; a Python traceback or a Java exception is counted on the Overview's Is anything failing? answer.
If it went wrong
- "no way to start this app was found" — set Runs with and the App module (or your own start command) in Settings, then Build again.
- A Python package fails to build — install the build tools on Software → Python, then deploy again.
- The version never goes live and the log says it listens on another address — your own start command listens on
0.0.0.0or a fixed port: use127.0.0.1and$PORT. - A Java app is stopped before it answers — raise Start time allowed in Settings, or give it more memory.
- "this Gradle project has no Gradle wrapper" — add it (
gradle wrapper), commit it, deploy again. - A reverse proxy answers 502 — the target does not answer from this server: press Test the target. A name that does not resolve here, a firewall on the other server, or an app that is stopped are the usual reasons.
- A reverse proxy answers 403 "has no users yet" — its login is on: add a user, or turn the login off.
- After an operating-system upgrade a Python website does not start — deploy it once: its virtual environment is made again with the new Python.
Something unclear?Ask Tia