Iris keeps its configuration in one plain struct: iris.Options. You build
the value in code, load it from YAML, or read it from an environment variable,
then hand it to iris.New or iris.NewBuilder. Every field is optional
because the framework merges what you provide with its defaults. This chapter
walks through the struct, the merge rules that make partial configuration
safe, the loading helpers, the router and context option tours, and the
server options passed to app.Run.
Background
Configuration is every value your program needs that is not part of its logic: which port to listen on, how verbose the logs are, which database to talk to, which password unlocks the admin account. The same compiled binary typically runs in several environments (your laptop, a staging server, the production cluster) and each needs different values. If those values live in the source code, every difference forces a rebuild, and every credential becomes visible to anyone who can read the code. The twelve-factor methodology, a widely cited set of principles for deployable applications, distills this into one rule: keep configuration in the environment, so the code stays identical across deployments and only the surrounding values change.
Two carriers for configuration appear throughout this chapter. A YAML
file is a plain-text format for structured data: key: value pairs,
nesting expressed by indentation, lists introduced with -. People can
read and edit it easily, which makes it the usual choice for settings that
live next to the code, such as development defaults. An environment
variable is a named string the operating system hands a process when it
starts. It is set outside the program (by the shell, a container runtime,
a systemd unit or an orchestration tool) and read at run time with a call
like os.Getenv. Because every deployment platform knows how to inject
environment variables, they are the natural channel for anything that
changes per deployment.
A secret is a configuration value that grants access: a password, an API key, a signing key, a database connection string. Secrets never belong in a repository, even a private one. Version control remembers everything, so a key committed once stays in the history long after the file is “fixed”; clones, forks, backups and CI machines all receive a copy; and revoking a leaked key is far more painful than never leaking it. The safe home for secrets is the environment, filled either by hand or by a secret manager (a vault service that stores secrets encrypted and injects them at deploy time).
Iris makes this discipline cheap to follow. One struct, iris.Options,
holds all framework settings; it merges with sensible defaults, loads from
YAML or a single environment variable, and embeds into your own config
struct. What disappears from your code is the configuration plumbing
itself: no flag-parsing forest, no per-environment build tags, no
hand-rolled merge of defaults against overrides.
The Options Struct
package main
import "github.com/kataras/iris/v14"
func main() {
app := iris.New(iris.Options{
Name: "bookstore",
LogLevel: "debug",
Host: "example.com",
})
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{"status": "ok"})
})
app.Listen(":8080")
}iris.Options has five fields, all optional:
| Field | Type | Purpose |
|---|---|---|
Name | string | Application name, used as the log prefix of the application’s logger. |
LogLevel | string | "disable", "fatal", "error", "warn", "info" (default) or "debug". |
Host | string | The trusted domain the server runs on, e.g. "example.com". Also copied to Router.Host, which subdomain routing relies on. |
Router | iris.RouterOptions | Routing behavior for all routes. |
Context | iris.ContextOptions | Request and response handling for all routes. |
Options implements the iris.Configurator interface, and iris.New
accepts any number of configurators. For imperative setup steps you can also
pass an iris.ConfiguratorFunc(func(app *iris.Application) { ... }), which
runs against the application instance at construction time.
Merge Semantics
An Options literal passed to iris.New merges with iris.DefaultOptions:
every field left at its zero value is filled from the defaults. A partial
literal therefore configures only what it sets. The example above changes the
name and the log level; the charset stays "utf-8", the multipart form limit
stays 32 MB, and so on.
Two refinements to that rule matter in practice.
A non-nil empty slice or map is an explicit “none”. nil means “unset,
give me the default”, while an allocated empty value means “I want nothing
here”. The field where the distinction is visible out of the box is
RemoteAddrPrivateSubnets, whose default is a list of common private IP
ranges:
import (
"github.com/kataras/iris/v14"
"github.com/kataras/iris/v14/core/pkg/netutil"
)
app := iris.New(iris.Options{
Context: iris.ContextOptions{
// nil would inherit the default private ranges;
// an empty non-nil slice clears them.
RemoteAddrPrivateSubnets: []netutil.IPRange{},
},
})A meaningful zero cannot be expressed at construction. For scalar fields
the merge cannot tell “unset” apart from “deliberately zero”. Setting
URLParamSeparator: "" in the literal does nothing, because the empty string
counts as unset and is replaced by the default ",". The escape hatch is
per-request mutation: ctx.Options() returns a request-scoped copy of the
context options that a middleware may edit freely, and the change dies with
the request:
app.Use(func(ctx iris.Context) {
// Treat "a,b" as one value instead of splitting on commas,
// for this request only.
ctx.Options().URLParamSeparator = ""
ctx.Next()
})The mechanism is not limited to zeros. Any handler can adjust the context
options for the request it serves. A route group that accepts large uploads,
for example, can raise BodyMaxMemory in its middleware while the rest of
the application keeps the strict global cap.
Defaults and EditOptions
iris.DefaultOptions is the exported default configuration. When you want
the defaults plus a few edits, iris.Defaults reads better than a literal
because you modify the defaults in place:
app := iris.New(iris.Defaults(func(opts *iris.Options) {
opts.LogLevel = "debug"
opts.Context.RemoteAddrHeaders = []string{"X-Forwarded-For"}
}))After construction, app.EditOptions applies further changes:
app := iris.New()
app.EditOptions(func(opts *iris.Options) {
opts.LogLevel = "warn"
opts.Context.BodyMaxMemory = 8 * iris.MB
})EditOptions must run before the application is built, that is before
Build, Listen or Run. Once built it only logs a warning and the change
may not be applied, so never call it from inside a request handler. For
per-request changes use ctx.Options() as shown above.
YAML Files and the SERVER_CONFIG Variable
Two helpers load Options from the outside world.
iris.YAML reads a YAML file, applies it over the defaults and panics when
the file is missing or malformed. It fits programs where the file is
required:
app := iris.New(iris.YAML("config.yml"))The YAML keys follow the struct’s YAML tags, which are CamelCase and nest the router and context sections:
# config.yml
Name: bookstore
LogLevel: info
Host: example.com
Router:
ForceLowercaseRouting: true
FireMethodNotAllowed: true
Context:
BodyMaxMemory: 4194304 # 4 MB
RemoteAddrHeaders:
- X-Forwarded-For(*Options).Bind is environment-first: it reads the SERVER_CONFIG
environment variable as a full YAML document and only falls back to the given
file when the variable is empty. It returns an error instead of panicking:
package main
import (
"log"
"github.com/kataras/iris/v14"
)
func main() {
var opts iris.Options
if err := opts.Bind("config.yml"); err != nil {
log.Fatal(err)
}
app := iris.New(opts)
app.Listen(":8080")
}One container variable can carry the entire configuration, so a deployment
does not need to mount files. Fields the document leaves out still inherit
the defaults when the value reaches iris.New.
All four inputs meet at the same merge, and the two rules that catch people out are consequences of it: an empty-but-non-nil collection survives, and a meaningful zero does not.
Router Options
iris.RouterOptions controls matching behavior for all routes. Every boolean
defaults to false.
| Field | Effect |
|---|---|
Host | The server’s host, including a non-standard port. Set automatically from Options.Host; subdomain routing depends on it. |
DisablePathCorrection | By default a request to /home/ is redirected (301) to a registered /home. Set to true to disable. |
DisablePathCorrectionRedirection | With path correction on, execute the corrected route’s handler directly instead of redirecting. |
EnablePathIntelligence | Redirect not-found GET requests to the closest registered path, e.g. /cont to /contact. |
EnablePathEscape | Keep encoded characters such as %2F inside path parameters. |
ForceLowercaseRouting | Register and match all paths lowercased. |
AllowQuerySemicolons | Convert unescaped ; in the query to & (the pre-Go 1.17 behavior). |
EnableDynamicHandler | Allow adding routes at serve time via RefreshRouter, at the cost of a routing mutex on every request. |
FireMethodNotAllowed | Answer 405 instead of 404 when the path matches but the method does not. A builder application answers it with the rest/errors JSON envelope; for your own body, register HandleNoMethod from a Configurator passed to NewBuilder, because the builder fills that slot at Build (Chapter 3, Chapter 4). |
app := iris.New(iris.Options{
Router: iris.RouterOptions{
ForceLowercaseRouting: true,
FireMethodNotAllowed: true,
},
})Context Options
iris.ContextOptions configures request and response handling. The same
struct is editable per request through ctx.Options().
Request fields:
| Field | Default | Effect |
|---|---|---|
URLParamSeparator | "," | Separator used to split multi-value entries in ctx.Query(), ctx.Form() and ctx.PostForm(). |
FireEmptyFormError | false | Make ReadForm, ReadQuery and ReadBody return iris.ErrEmptyForm on empty form data instead of succeeding silently. |
TimeFormat | "Mon, 02 Jan 2006 15:04:05 GMT" | Datetime parsing format, used for example by ctx.CheckIfModifiedSince. |
PostMaxMemory | 32 MB | Memory cap for multipart form parsing. |
BodyMaxMemory | 0 (no limit) | Byte cap for Body, ReadJSON, ReadJSONStream, ReadMultipartRelated and the readers built on them, including ctx.BindJSON. Exceeding it fails with the context package’s ErrRequestEntityTooLarge, which rest/errors maps to 413. |
RemoteAddrHeaders | none | Header names trusted to reveal the real client IP in ctx.RemoteAddr(). Empty by default: no X- header is trusted unless you list it. |
RemoteAddrHeadersForce | false | Use the first header entry as the client IP even when it falls inside a private subnet. |
RemoteAddrPrivateSubnets | common private ranges | IP ranges treated as private when resolving the client IP. |
SSLProxyHeaders | none | Header name/value pairs that mark a request as HTTPS for ctx.IsSSL(), e.g. {"X-Forwarded-Proto": "https"}. |
HostProxyHeaders | none | Headers that may carry the proxied hostname for ctx.Host(). |
RequestValidator | nil | Called after body decoding on ReadJSON, ReadForm, ReadQuery and friends. |
Response fields:
| Field | Default | Effect |
|---|---|---|
Charset | "utf-8" | Character set appended to rendered Content-Type headers. |
ErrorHandler | framework default | Handles errors produced while writing responses through JSON, XML, Render and the other writers. |
DisableErrorHandler | false | Turn the write-error handler off entirely. |
In production, requests often reach your server through a reverse
proxy: an intermediary such as a load balancer, nginx or a CDN that
accepts the client’s connection and forwards it to your application. To
your program every request then appears to come from the proxy’s own IP,
often over plain HTTP, so proxies attach headers such as
X-Forwarded-For (the original client IP) and X-Forwarded-Proto (the
original scheme). These headers are ordinary text that any client could
forge, which is why Iris trusts none of them until you explicitly list the
ones your own proxy sets.
A typical proxy-aware production configuration:
app := iris.New(iris.Options{
Context: iris.ContextOptions{
BodyMaxMemory: 8 * iris.MB,
RemoteAddrHeaders: []string{"X-Forwarded-For", "X-Real-Ip"},
SSLProxyHeaders: map[string]string{
"X-Forwarded-Proto": "https",
},
},
})BodyMaxMemory is the application-wide cap; for per-route limits use the
bodylimit middleware instead (Chapter 12).
RequestValidator accepts any value with a Struct(any) error method. The
third-party github.com/go-playground/validator/v10 package satisfies it
directly:
import "github.com/go-playground/validator/v10"
app.EditOptions(func(opts *iris.Options) {
opts.Context.RequestValidator = validator.New()
})See Chapter 5 for the body readers themselves.
Embedding Options in Your Own Config
Real applications have settings of their own next to the framework’s.
Embedding iris.Options inline in the application’s config struct makes one
YAML document configure both, and the loader below is that shape written out
in full, because it is worth seeing once. It is not what the tutorial or the
rest of this book does any more: they embed iris.BaseConfiguration, which
carries Host, Port, AllowOrigin, EnableCompression, RequestLog and
the framework options under an Iris key, and they load it with the function
the framework ships. The next section is
the version to copy into a new service.
package main
import (
"fmt"
"log"
"os"
"gopkg.in/yaml.v3"
"github.com/kataras/iris/v14"
)
// Config holds the whole application configuration.
// AdminPassword is yaml:"-" on purpose: secrets come from the
// environment, never from a committed file.
type Config struct {
iris.Options `yaml:",inline"`
Addr string `yaml:"Addr"`
Env string `yaml:"Env"`
AdminUser string `yaml:"AdminUser"`
AdminPassword string `yaml:"-"`
}
// LoadConfig starts from working defaults, overlays YAML from the
// APP_CONFIG environment variable or the config.yml file (both
// optional), then reads the secrets from the environment.
func LoadConfig() (Config, error) {
cfg := Config{
Options: iris.DefaultOptions,
Addr: ":8080",
Env: "development",
AdminUser: "admin",
}
if raw := os.Getenv("APP_CONFIG"); raw != "" {
if err := yaml.Unmarshal([]byte(raw), &cfg); err != nil {
return cfg, fmt.Errorf("config from env: %w", err)
}
} else if data, err := os.ReadFile("config.yml"); err == nil {
if err = yaml.Unmarshal(data, &cfg); err != nil {
return cfg, fmt.Errorf("config.yml: %w", err)
}
}
cfg.AdminPassword = os.Getenv("ADMIN_PASSWORD")
if cfg.AdminPassword == "" {
return cfg, fmt.Errorf("ADMIN_PASSWORD is required")
}
return cfg, nil
}
func main() {
cfg, err := LoadConfig()
if err != nil {
log.Fatal(err)
}
app := iris.New(cfg.Options)
// With the builder instead: iris.NewBuilder(cfg.Options)...
// (see Chapter 3).
app.Listen(cfg.Addr)
}The committed config.yml contains settings only, no secrets:
# Committed settings only. The admin password comes from
# the ADMIN_PASSWORD environment variable.
Name: bookstore
LogLevel: info
Addr: ":8080"
Env: development
AdminUser: adminThe yaml:",inline" tag makes the framework keys (Name, LogLevel,
Host, Router, Context) appear at the top level of the same document as
your own keys. The loading order mirrors Options.Bind: environment variable
first, file as the development fallback. Never commit credentials, key
material or connection strings, even in private repositories; the
environment, or a secret manager feeding it, is their only home.
The loader the framework ships
That LoadConfig function is the same forty-five lines in every service
that has one, so the framework has its own:
type Configuration struct {
iris.BaseConfiguration `yaml:",inline"`
DatabaseURL string `yaml:"DatabaseURL"`
}
func main() {
c, err := iris.LoadConfiguration[Configuration](
"MY_SERVICE_CONFIG", "configuration.yml")
if err != nil {
log.Fatal(err)
}
app := iris.New(c.Iris)
// ...routes...
app.Listen(c.Addr())
}iris.LoadConfiguration[T] reads a YAML document into T from the named
environment variable when it is set, otherwise from the file. The variable
may hold either the document or a path to it: a value naming an existing
file is read as a file and anything else is decoded as YAML, so the same
binary works whether the deployment mounts the configuration or passes it
inline. It is not a generic Options.Bind; that one is hardcoded to
SERVER_CONFIG and always reads its value as a document.
If T (or *T) has a BindEnv() method it runs after decoding, which
is what makes the environment win over the document. Embedding
iris.BaseConfiguration gets that for the fields every service shares:
| Field | YAML key | Meaning |
|---|---|---|
Host | Host | The interface to listen on; empty means every interface. Not Iris.Host, which is the trusted domain the router matches subdomains against |
Port | Port | The TCP port; zero means 8080 |
AllowOrigin | AllowOrigin | The origin line, as Builder.AllowOrigin accepts it |
EnableCompression | EnableCompression | As Builder.Compression accepts it |
RequestLog | RequestLog | As Builder.LogRequests accepts it |
Iris | Iris | The framework Options, as iris.New accepts them |
Its BindEnv reads HOST, PORT, HTTP_PLATFORM_PORT and
ALLOW_ORIGIN. HTTP_PLATFORM_PORT wins over PORT, because a hosting
platform that picks the port passes it there while PORT may still carry
the value baked into the image. A non-numeric port is ignored rather than
turned into zero, so a typo cannot silently move the server to 8080.
Addr() returns the host:port string for app.Listen, joined so that
an IPv6 host is bracketed correctly.
There is deliberately no app.Serve(host, port). Listen and Run stay
the only two entry points, and Addr() is the part that was missing.
Multi-Environment Setups
Because the whole configuration travels as one YAML document, environments do not need file hierarchies or override matrices. Keep the development file in the repository, and let each deployment carry its own document in the environment variable:
# deploy/production.yml, injected as APP_CONFIG
Name: bookstore
LogLevel: warn
Host: api.example.com
Addr: ":8080"
Env: production
Context:
RemoteAddrHeaders:
- X-Forwarded-For
SSLProxyHeaders:
X-Forwarded-Proto: https# The deployment pipeline sets the variable; locally you can do:
APP_CONFIG="$(cat deploy/production.yml)" ./serverA Kubernetes ConfigMap, a systemd unit or a compose file sets the same variable the same way. Unset fields still merge with the defaults, so each document stays as small as the differences between environments.
Server Options
Serving is configured separately from the application, at the call to
app.Run. app.Listen(addr) is the shorthand; the two are equivalent:
app.Listen(":8080")
// is the same as:
app.Run(iris.ServerOptions{
Addr: ":8080",
Network: "tcp",
KeepAlive: 3 * time.Minute,
SocketSharding: true,
})The fields of iris.ServerOptions:
| Field | Purpose |
|---|---|
Addr | Listen address in "host:port" form. Empty means ":http", or ":https" when TLSConfig is set. |
Network | "tcp" (default), "tcp4", "tcp6", "unix" or "unixpacket". |
KeepAlive | Keep-alive period for accepted connections; negative disables keep-alives. |
SocketSharding | SO_REUSEPORT (SO_REUSEADDR on Windows) for multi-CPU scaling. |
Hosts | Hostnames the TLS certificate is valid for; falls back to the application’s Host, then to Addr. |
TLSConfig | Custom *tls.Config. |
CertFile, KeyFile | Paths to PEM-encoded certificate and key files. |
Letsencrypt | Automatic HTTPS via Let’s Encrypt. |
Ngrok, SSHTunnel | Expose the local server through an ngrok or SSH reverse tunnel (Chapter 19). Ngrok needs the blank import _ "github.com/kataras/iris/v14/core/server/ngrok"; without it the server refuses to start. |
Timeout | *iris.TimeoutOptions, see below. |
LogWriter | Destination for errors surfaced by the underlying http.Server. |
DisableStartupLog | Suppress the startup message. |
DisableInterruptHandler | Skip the built-in interrupt handler that shuts the server down gracefully on CTRL+C. |
TLS (Transport Layer Security) is the encryption layer that turns HTTP
into HTTPS. A server proves its identity with a certificate (a file,
signed by a trusted certificate authority, that binds a hostname to a
public key) and keeps the matching private key to itself. Let’s Encrypt
is a certificate authority that issues certificates free of charge through
an automated protocol; the Letsencrypt option drives that protocol for
you, so the server obtains and renews its own certificates instead of you
installing files.
Use one TLS mechanism at a time: TLSConfig, the CertFile/KeyFile pair,
Letsencrypt, Ngrok or SSHTunnel. The one sanctioned combination is
Letsencrypt with CertFile/KeyFile, where the files act as a local
fallback when the Let’s Encrypt challenge fails.
Serving TLS from certificate files:
app.Run(iris.ServerOptions{
Addr: ":443",
CertFile: "server.crt",
KeyFile: "server.key",
})Automatic HTTPS:
app.Run(iris.ServerOptions{
Addr: ":443",
Hosts: []string{"api.example.com"},
Letsencrypt: true,
})Timeouts
A timeout is the maximum time you allow a network operation before giving up on it. Servers need timeouts because clients can be slow or hostile on purpose: a connection trickling one byte per second would otherwise hold memory and a file descriptor forever. Keep-alive, mentioned below, is the practice of reusing one TCP connection for several requests instead of opening a fresh connection each time; the idle timeout bounds how long such a reused connection may sit waiting for its next request.
iris.TimeoutOptions holds the standard net/http server timeouts plus an
optional handler timeout:
| Field | Effect |
|---|---|
ShutdownOnInterruptTimeout | How long graceful shutdown may take on interrupt. Defaults to 15 seconds. |
ServerTimeout | One knob from which the read-header, write and idle timeouts are derived. |
ReadTimeout | Maximum duration to read the whole request, body included. |
ReadHeaderTimeout | Time allowed to read request headers; handlers keep control over body deadlines. |
WriteTimeout | Maximum duration for writing the response. |
IdleTimeout | Keep-alive wait between requests. |
HandlerTimeout | Wraps the router in http.TimeoutHandler when greater than zero. |
HandlerTimeoutMessage | HTML body sent when the handler timeout fires. |
Start from the iris.Timeout() builder so the shutdown timeout and the
default timeout message stay in place, then set what you need:
package main
import (
"time"
"github.com/kataras/iris/v14"
)
func main() {
app := iris.New()
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{"status": "ok"})
})
app.Run(iris.ServerOptions{
Addr: ":8080",
Timeout: iris.Timeout().
WithServerTimeout(30 * time.Second).
WithHandlerTimeout(15 * time.Second),
})
}Unless DisableInterruptHandler is set, the server catches the interrupt
signal, stops accepting connections and waits up to
ShutdownOnInterruptTimeout for in-flight requests to finish.
Summary
iris.Options(Name,LogLevel,Host,Router,Context) is the whole application configuration; every field is optional.- Options merge with
iris.DefaultOptions: partial literals are safe, a non-nil empty slice or map means “explicitly none”, and meaningful zeros are set per request throughctx.Options()in a middleware. iris.Defaults(edits...)tweaks the defaults in place;app.EditOptionsapplies changes before the application is built, never from a handler.iris.YAML("config.yml")loads a required file;(*Options).Bindreads theSERVER_CONFIGenvironment variable first and falls back to a file.- Embed
iris.Optionsinto your own config struct withyaml:",inline"for one document that configures both the framework and your application; keep secrets in the environment only. - Serving is configured at
app.Run(iris.ServerOptions{...}): address, TLS (files, custom config or Let’s Encrypt), keep-alive, socket sharding and timeouts viairis.Timeout().
Further Reading
- The Twelve-Factor App: Config: the canonical statement of why configuration belongs in the environment, separate from code.
- OWASP Secrets Management Cheat Sheet: practical guidance on storing, injecting and rotating secrets.
- YAML 1.2 specification: the definitive
reference for the format read by
iris.YAMLandOptions.Bind. - yaml.v3 package documentation: the Go YAML library used in this chapter’s embedding example.
- MDN: X-Forwarded-For: how proxy forwarding headers work and why they must not be trusted blindly.
- Let’s Encrypt documentation: how automatic HTTPS certificate issuance and renewal work.