Chapters

Hide chapters

Server-Side Swift with Vapor

Third Edition · iOS 13 · Swift 5.2 - Vapor 4 Framework · Xcode 11.4

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section I: Creating a Simple Web API

Section 1: 13 chapters
Show chapters Hide chapters

35. Production Concerns & Redis

One of the most exciting parts of programming is sharing what you’ve created with the world. For web applications, this usually means deploying your project to a server that is accessible via the internet.

Web servers can be dedicated machines in a data center, containers in a cloud or even a Raspberry Pi sitting in your closet. As long as your server can run Swift and has a connection to the internet, you can use it to deploy Vapor applications.

In this chapter, you’ll learn the advantages and disadvantages of some common deployment methods for Vapor. You’ll also learn how to properly optimize, configure and monitor your applications to increase efficiency and uptime.

Using environments

Every instance of Application has an associated Environment. Each environment has a String name. Common environments include: production, development, and testing. You can retrieve the current environment from the environment property of Application.

print(req.application.environment) // "production"

For the most part, the environment is there for you to use as you wish while configuring your application.

However, some parts of Vapor will behave differently when running in a release environment. Some differences include hiding debug information in 500 errors and reducing the verbosity of error logs.

Because of this, make sure you are using the production environment when running your application in production.

Choosing an environment

Most templates include code to detect the current environment when the application runs. If you open main.swift in your project’s Run module, you’ll see something similar to the following:

import App
import Vapor

var env = try Environment.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
try configure(app)
try app.run()

This code calls Environment.detect(), which parses the command line arguments passed to your application and returns the environment specified. If you don’t specify an environment, Vapor uses development by default. You can specify the environment using the --env flag followed by the name of the environment.

You can do this when running your application’s executable from the command line using swift run.

swift run Run serve --env development

You can also specify the environment when running your application from within Xcode using the scheme editor.

Vapor supports shortcuts like prod for production and dev for development. It also supports the -e abbreviation for --env.

$ swift run Run serve -e prod

Compiling with optimizations

While developing your application, you’ll usually compile code using Swift’s debug build mode. Debug build mode is fast and includes useful debug information in the resulting binary. Xcode can use this information later to provide more information about fatal errors and breakpoint debugging.

For production deployments, you should use Swift’s release build mode. When building in release mode, Swift spends more time analyzing and optimizing your program. While this increases the overall build time, it’s well worth the performance improvements at runtime. Swift also removes debugging information from the resulting binary, making it smaller.

Vapor and Swift NIO may also behave slightly differently in release build mode. A common pattern in these packages is to convert recoverable developer errors into fatal errors while in debug mode. This helps the developer track down common errors quickly during development without compromising stability in production.

This section shows you how to enable release build mode, both in Xcode and directly using SwiftPM. It also shows you how to run your tests in release mode. This can be useful for tests that depend on runtime performance.

Building release in Xcode

You enable release build mode in Xcode using the scheme editor. To build in release mode, edit the scheme for your app’s executable target. Then, select Release under Build Configuration.

To test in release mode, again edit the scheme for your app’s executable target. Then, select Test from the left side of the scheme editor and change Build Configuration mode to Release.

Building release using SwiftPM

When deploying to Linux, you’ll need to use SwiftPM to compile release executables since Xcode is not available. By default, SwiftPM compiles in debug build mode. To specify release mode, append -c release to your build command.

swift build -c release

When the build finishes, the compiler prints the path of the resulting executable to the terminal. You can copy and paste that path to run your application.

If you visit the build folder, you may notice additional files exist alongside your executable binary. Among these files are any shared libraries (.dylib on macOS and .so on Linux) produced by the build process. These shared libraries are required for your executable to run.

You can also run your tests in release mode with SwiftPM.

swift test -c release

Note that some features, like @testable import, may not be available when testing in release mode.

Note on testing

Building and testing your code regularly in production-like environments is important for catching issues early. Some modules you will use, like Foundation, have different implementations depending on the platform. Subtle differences in implementation can cause bugs in your code. Sometimes, an API’s implementation may not yet exist for a platform. Container environments like Docker help you address this by making it easy to test your code on platforms different from your host machine, such as testing on Linux while developing on macOS.

Using Docker

Docker is a great tool for testing and deploying your Vapor applications. Deployment steps are coded into a Dockerfile you can commit to source control alongside your project. You can execute this Dockerfile to build and run instances of your app locally for testing or on your deployment server for production. This has the advantage of making it easy to test deployments, create new ones and track changes to how your deploy your code.

See Chapter 33, “Deploying with Docker,” for more information.

Process monitoring

To run a Vapor application, you simply need to launch the executable generated by SwiftPM.

swift build -c release
.build/release/Run serve -e prod

While this works great for testing, it has one major problem: What happens if your application crashes? In that case, you would need to log in to your server and restart it manually. Fortunately, process monitors can help remedy this.

Supervisor

Supervisor, also called supervisord, is a popular process monitor for Linux. This program allows you to register processes that you would like to start and stop on demand. If one of those processes crashes, Supervisor will automatically restart it for you. It also makes it easy to store the process’s stdout and stderr in /var/log for easy access.

Supervisor is usually installed using APT on Ubuntu but may vary depending on your deployment method.

apt-get install supervisor

Once installed, Supervisor can be started using Ubuntu’s systemctl command.

systemctl restart supervisor

Supervisor’s configuration files are stored in /etc/supervisor/conf.d. Create a new file there to manage your Vapor app called my-app.conf.

// 1
[program:my-app]
command=/path/to/my-app/.build/release/Run serve -e prod
// 2
autostart=true
autorestart=true
// 3
stderr_logfile=/var/log/my-app.err.log
stdout_logfile=/var/log/my-app.out.log

Here’s a breakdown of what this configuration file does:

  1. Declare a new Supervisor program that launches your application’s Run executable using the serve command and production environment.
  2. Enable auto-start and auto-restart, which ensures your application is always running when the server is on.
  3. Configure Supervisor to direct your application’s stderr and stdout to log files.

Now that you’ve added the configuration file, run the following command to update Supervisor.

supervisorctl reread
supervisorctl update

Your application should now be running. If the application crashes, Supervisor will notice this and immediately attempt to restart it.

Systemd

Another alternative that doesn’t require you to install additional software is called systemd. It’s a standard part of the Linux versions that Swift supports. For more on how to configure your app using systemd, see Chapter 34, “Deploying with AWS”.

Reverse Proxies

Regardless of where or how you deploy your Vapor application, it’s usually a good idea to host it behind a reverse proxy like nginx. nginx is an extremely fast, battle tested and easy-to-configure HTTP server and proxy. While Vapor supports directly serving HTTP requests, proxying behind nginx can provide increased performance, security, and ease-of-use. nginx, for example, can provide support for TLS (SSL), public file serving and HTTP/2.

Installing Nginx

nginx is usually installed using APT on Ubuntu but may vary depending on your deployment method.

apt-get update
apt-get install nginx

Once installed, nginx can be started using Ubuntu’s systemctl command.

systemctl start nginx
systemctl restart nginx
systemctl stop nginx

Once started, you can create a new site configuration in /etc/nginx/sites-enabled. Take a look at the example nginx configuration file below:

server {
  ## 1
  server_name hello.com;

  ## 2
  listen 80;

  ## 3
  root /home/vapor/Hello/Public/;
  try_files $uri @proxy;
  
  ## 4
  location @proxy {
    ## 5
    proxy_pass http://127.0.0.1:8080;

    ## 6
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    ## 7
    proxy_connect_timeout 3s;
    proxy_read_timeout 10s;
  }
}

Here’s what each line of the nginx configuration does:

  1. Specify this configuration is used for requests to hello.com. You can list multiple server names here.
  2. Specify this configuration is used for requests to port 80, the default HTTP port.
  3. Specify a document root for this server. Any requests to hello.com/* which match file names in this folder will be served directly by nginx, bypassing your Vapor application.
  4. Specify this server should be a reverse proxy.
  5. Pass all requests to the Vapor application bound to 127.0.0.1 port 8080.
  6. Specify special headers to add to the incoming request. These headers help Vapor maintain information about the connected client.
  7. Specify connection and read timeouts for your server.

Once you’ve saved the configuration file, restart nginx to enable the new site. Next, ensure your Vapor server is running at the hostname and port specified in your configuration. You should now be able to access your Vapor server through nginx.

Logging

Using Swift’s print method for logging is great during development and can even be a suitable option for some production use cases. Programs like Supervisor help aggregate your application’s print output into files on your server that you can access as needed.

However, there may be situations where you want to collect your logs in a different way. For example, maybe you would prefer to collect logs and send them to a remote API for storage. You may also want to specify each log’s importance, so you know how to treat it. Vapor uses SwiftLog (https://github.com/apple/swift-log) to provide a consistent API for you and all packages you use to build upon.

Using logging is easy; simply import Vapor and access a Logger from your Request or Application.

app.get("log-test") { req -> HTTPStatus in
    req.logger.info("The route was called")
    return .ok
}

The logger has several log level methods available:

  • trace: Log any and all information. Used to trace specific problems.
  • debug: Used to debug problems.
  • info: Indicates an infrequent event has occurred.
  • notice: Used to notify about specific events or status that should be noted but not treated as an error.
  • warning: Indicates something should be fixed.
  • error: Indicates something went wrong.
  • critical: Fatal errors. Execution must be canceled.

By default, accessing a Logger will yield a ConsoleLogger, which outputs your logs to the console using terminal colors to specify log level.

However, there are several other implementation for SwiftLog for you to choose, which can be found at https://github.com/apple/swift-log#selecting-a-logging-backend-implementation-applications-only.

Horizontal scalability

Finally, one of the most important concerns in designing a production-ready app is that of scalability. As your application’s user base grows and traffic increases, how will you keep up with demand? What will be your bottlenecks? When first starting out, a reasonable solution can be to increase your server’s resources as traffic increases — adding RAM, better CPU, more disk space, etc. This is commonly referred to as scaling vertically.

Where vertical scaling falls apart is when your application’s requirements start to exceed the power of a single server. Eventually, if your application grows large enough, you may need to scale to multiple servers. This is called horizontal scaling. However, horizontal scaling is not only useful when you’ve exhausted your ability to scale vertically. Scaling to multiple cheap servers can be more cost effective than a single expensive server.

Load balancing

Now that you understand some of the benefits of horizontal scaling, you may be wondering how it actually works. The key to this concept is load balancers. Load balancers are light-weight, fast programs that sit in front of your application’s servers. When a new request comes in, the load balancer chooses one of your servers to send the request to.

If one of the servers is unhealthy — responding slowly or returning errors — the load balancer can temporarily stop sending requests to that server.

In the diagram above, the load balancer receives a message from the client and decides to forward the request to App #3. The application generates a response for the request, and the load balancer delivers that response back to the client.

While the basics of horizontal scaling are simple, you should understand some common pitfalls that can prevent your application from being scaled this way. Most commonly, these problems relate to storing information locally on the server.

To better understand this, take the following example of a profile picture upload endpoint that saves the image to disk:

When Client A uploads its profile image to the API, the load balancer directs the request to App #2. This application processes the request and saves the image to the server’s disk. Later, when Client B attempts to fetch that image, the load balancer directs the request to App #3. The server running App #3 does not know about that image, so it returns an error. It’s possible that Client B could have been directed to App #2 to successfully fetch the image, but that would have been pure luck.

Other common examples of this problem are in-memory session caches and SQLite databases. A general solution to this problem is to use shared storage for your application’s common data. This means data that any instance of your application might need to access. If the data is private to the server — for example, an API response cache — there is no problem storing it locally.

There are a plethora of tools available for you to make your application scalable. For file upload, there are APIs like Amazon Web Service’s S3 buckets that let you store and fetch files from a single, remote source. You may also be able to configure your servers with a shared drive for file storage, as the following figure shows:

In the example above, Client B’s request for the image succeeds since both App #2 and App #3 have access to the same shared drive. For databases and sessions, you can use non-file based databases like Redis, MySQL, PostgreSQL, MongoDB and more. These databases run on a separate server that all of your application instances can access.

If you think your application will need to handle a lot of traffic, or it has the potential to grow quickly, keep horizontal scalability in mind as you design and write code.

Sessions with Redis

To demonstrate how this works in an app, download the starter project for this chapter. The project is based on the TIL app from the first sections of this book. Open the project in Xcode and build the application.

Note: As in previous chapters, you need to set the custom working directory for the project.

When a user logs in to the website, the application stores the user’s ID in an associated session. Currently the application stores sessions in memory. This presents a couple of problems:

  • When you restart the application, you lose all your sessions. Any logged in users will have to log in again.
  • If you scale your application horizontally, the sessions aren’t shared. If a user logs in to server #1 and the next request from that user goes to server #2, it doesn’t know about the session, so the user can’t access any protected routes. Logging into server #2 overwrites the session information for server #1, thereby losing that session. As you scale horizontally, the chance this causes problems increases.

You can solve this by moving the sessions into a database. Redis is a fast, in-memory database that has many uses, and it’s a great choice for this use case. If all instances of the application use Redis, they can share sessions.

In Xcode, open configure.swift. The starter project already has Redis configured as a dependency in Package.swift. Below import Leaf, add the following:

import Redis

This allows you to see Redis functions and types. Next, configure the Redis database in your application. Below app.databases.use(...) add the following:

// 1
let redisHostname = Environment
  .get("REDIS_HOSTNAME") ?? "localhost"
// 2
let redisConfig = 
  try RedisConfiguration(hostname: redisHostname)
// 3
app.redis.configuration = redisConfig

Here’s what this does:

  1. Set the hostname to the REDIS_HOSTNAME environment variable, if it exists. Otherwise, default to localhost. This allows you to inject the hostname for hosting solutions.
  2. Create a RedisConfiguration using the hostname.
  3. Set the RedisConfiguration on the application’s Redis service.

Next, add the following after app.views.use(.leaf):

app.sessions.use(.redis)

This tells the application to use Redis when storing session data.

Finally, move app.middleware.use(app.sessions.middleware) to below app.sessions.use(.redis). This ensures that the sessions middleware uses the Redis sessions configuration.

That’s all that’s required to use Redis with sessions!

In Terminal, enter the following to start the databases:

# 1
docker run --name postgres \
  -e POSTGRES_DB=vapor_database \
  -e POSTGRES_USER=vapor_username \
  -e POSTGRES_PASSWORD=vapor_password \
  -p 5432:5432 -d postgres
# 2
docker run --name redis -p 6379:6379 -d redis

Here’s what this does:

  1. Start the PostgreSQL database:
  • Run a new container named postgres.
  • Specify the database name, username and password through environment variables.
  • Allow applications to connect to the PostgreSQL server on its default port: 5432.
  • Run the server in the background as a daemon.
  • Use the Docker image named postgres for this container. If the image isn’t present on your machine, Docker automatically downloads it.
  1. Start the Redis database:
  • Run a new container named redis.
  • Allow applications to connect to the Redis server on its default port: 6379.
  • Run the server in the background as a daemon.
  • Use the Docker image named redis for this container. If the image isn’t present on your machine, Docker automatically downloads it.

Build and run the application in Xcode. In your browser, navigate to http://localhost:8080/. Click Create An Acronym and the app redirects you to the log in page. Log in with the username admin and the password password. Click Create An Acronym and you can view the page:

In Xcode, stop and start the app and refresh the page in the browser. The application knows you’re still logged in as it stores the session in Redis instead of in-memory.

Where to go from here?

You now understand the common pitfalls to avoid when moving your Swift web application to production. It’s time to put the best practices and useful tools listed here to use. Here are some additional resources that should prove invaluable as you continue to hone your skills:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.