Repository: bpg/terraform-provider-proxmox

Issue:
Add support for `env` parameter in the `proxmox_virtual_environment_container` resource
**Is your feature request related to a problem? Please describe.**

Since PVE 9.1, a new parameter `env` is available on LXC containers to configure environment variables. Environment variables are then added to the conf file as `lxc.environment` entries.

**Describe the solution you'd like**

Add an `env` attribute to the `proxmox_virtual_environment_container` resources to manage configured environment variables.

**Describe alternatives you've considered**

N/A

**Additional context**

N/A


Discussion:

Solution patch:
diff --git a/docs/resources/virtual_environment_container.md b/docs/resources/virtual_environment_container.md
index af3302cf..12ef531e 100644
--- a/docs/resources/virtual_environment_container.md
+++ b/docs/resources/virtual_environment_container.md
@@ -141,6 +141,7 @@ output "ubuntu_container_public_key" {
         to `4`). When set to 0 a directory or zfs/btrfs subvolume will be created.
         Requires `datastore_id` to be set.
     - `mount_options` (Optional) List of extra mount options.
+- `environment_variables` - (Optional) A map of runtime environment variables for the container init process.
 - `initialization` - (Optional) The initialization configuration.
     - `dns` - (Optional) The DNS configuration.
         - `domain` - (Optional) The DNS search domain.
diff --git a/proxmox/nodes/containers/containers_types.go b/proxmox/nodes/containers/containers_types.go
index 347c6a88..b4fb5ab2 100644
--- a/proxmox/nodes/containers/containers_types.go
+++ b/proxmox/nodes/containers/containers_types.go
@@ -11,6 +11,7 @@ import (
 	"fmt"
 	"net/url"
 	"regexp"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -48,48 +49,52 @@ type CloneRequestBody struct {
 
 // CreateRequestBody contains the data for a user create request.
 type CreateRequestBody struct {
-	BandwidthLimit       *float64                 `json:"bwlimit,omitempty"              url:"bwlimit,omitempty"`
-	ConsoleEnabled       *types.CustomBool        `json:"console,omitempty"              url:"console,omitempty,int"`
-	ConsoleMode          *string                  `json:"cmode,omitempty"                url:"cmode,omitempty"`
-	CPUArchitecture      *string                  `json:"arch,omitempty"                 url:"arch,omitempty"`
-	CPUCores             *int                     `json:"cores,omitempty"                url:"cores,omitempty"`
-	CPULimit             *int                     `json:"cpulimit,omitempty"             url:"cpulimit,omitempty"`
-	CPUUnits             *int                     `json:"cpuunits,omitempty"             url:"cpuunits,omitempty"`
-	DatastoreID          *string                  `json:"storage,omitempty"              url:"storage,omitempty"`
-	DedicatedMemory      *int                     `json:"memory,omitempty"               url:"memory,omitempty"`
-	Delete               []string                 `json:"delete,omitempty"               url:"delete,omitempty"`
-	Description          *string                  `json:"description,omitempty"          url:"description,omitempty"`
-	DNSDomain            *string                  `json:"searchdomain,omitempty"         url:"searchdomain,omitempty"`
-	DNSServer            *string                  `json:"nameserver,omitempty"           url:"nameserver,omitempty"`
-	Features             *CustomFeatures          `json:"features,omitempty"             url:"features,omitempty"`
-	Force                *types.CustomBool        `json:"force,omitempty"                url:"force,omitempty,int"`
-	HookScript           *string                  `json:"hookscript,omitempty"           url:"hookscript,omitempty"`
-	Hostname             *string                  `json:"hostname,omitempty"             url:"hostname,omitempty"`
-	IgnoreUnpackErrors   *types.CustomBool        `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"`
-	Lock                 *string                  `json:"lock,omitempty"                 url:"lock,omitempty,int"`
-	MountPoints          CustomMountPoints        `json:"mp,omitempty"                   url:"mp,omitempty,numbered"`
-	PassthroughDevices   CustomPassthroughDevices `json:"dev,omitempty"                  url:"dev,omitempty,numbered"`
-	NetworkInterfaces    CustomNetworkInterfaces  `json:"net,omitempty"                  url:"net,omitempty,numbered"`
-	OSTemplateFileVolume *string                  `json:"ostemplate,omitempty"           url:"ostemplate,omitempty"`
-	OSType               *string                  `json:"ostype,omitempty"               url:"ostype,omitempty"`
-	Password             *string                  `json:"password,omitempty"             url:"password,omitempty"`
-	PoolID               *string                  `json:"pool,omitempty"                 url:"pool,omitempty"`
-	Protection           *types.CustomBool        `json:"protection,omitempty"           url:"protection,omitempty,int"`
-	Restore              *types.CustomBool        `json:"restore,omitempty"              url:"restore,omitempty,int"`
-	RootFS               *CustomRootFS            `json:"rootfs,omitempty"               url:"rootfs,omitempty"`
-	SSHKeys              *CustomSSHKeys           `json:"ssh-public-keys,omitempty"      url:"ssh-public-keys,omitempty"`
-	Start                *types.CustomBool        `json:"start,omitempty"                url:"start,omitempty,int"`
-	StartOnBoot          *types.CustomBool        `json:"onboot,omitempty"               url:"onboot,omitempty,int"`
-	StartupBehavior      *CustomStartupBehavior   `json:"startup,omitempty"              url:"startup,omitempty"`
-	Swap                 *int                     `json:"swap,omitempty"                 url:"swap,omitempty"`
-	Tags                 *string                  `json:"tags,omitempty"                 url:"tags,omitempty"`
-	Template             *types.CustomBool        `json:"template,omitempty"             url:"template,omitempty,int"`
-	TTY                  *int                     `json:"tty,omitempty"                  url:"tty,omitempty"`
-	Unique               *types.CustomBool        `json:"unique,omitempty"               url:"unique,omitempty,int"`
-	Unprivileged         *types.CustomBool        `json:"unprivileged,omitempty"         url:"unprivileged,omitempty,int"`
-	VMID                 *int                     `json:"vmid,omitempty"                 url:"vmid,omitempty"`
+	BandwidthLimit       *float64                    `json:"bwlimit,omitempty"              url:"bwlimit,omitempty"`
+	ConsoleEnabled       *types.CustomBool           `json:"console,omitempty"              url:"console,omitempty,int"`
+	ConsoleMode          *string                     `json:"cmode,omitempty"                url:"cmode,omitempty"`
+	CPUArchitecture      *string                     `json:"arch,omitempty"                 url:"arch,omitempty"`
+	CPUCores             *int                        `json:"cores,omitempty"                url:"cores,omitempty"`
+	CPULimit             *int                        `json:"cpulimit,omitempty"             url:"cpulimit,omitempty"`
+	CPUUnits             *int                        `json:"cpuunits,omitempty"             url:"cpuunits,omitempty"`
+	DatastoreID          *string                     `json:"storage,omitempty"              url:"storage,omitempty"`
+	DedicatedMemory      *int                        `json:"memory,omitempty"               url:"memory,omitempty"`
+	Delete               []string                    `json:"delete,omitempty"               url:"delete,omitempty"`
+	Description          *string                     `json:"description,omitempty"          url:"description,omitempty"`
+	DNSDomain            *string                     `json:"searchdomain,omitempty"         url:"searchdomain,omitempty"`
+	DNSServer            *string                     `json:"nameserver,omitempty"           url:"nameserver,omitempty"`
+	EnvironmentVariables *CustomEnvironmentVariables `json:"env,omitempty"                  url:"env,omitempty"`
+	Features             *CustomFeatures             `json:"features,omitempty"             url:"features,omitempty"`
+	Force                *types.CustomBool           `json:"force,omitempty"                url:"force,omitempty,int"`
+	HookScript           *string                     `json:"hookscript,omitempty"           url:"hookscript,omitempty"`
+	Hostname             *string                     `json:"hostname,omitempty"             url:"hostname,omitempty"`
+	IgnoreUnpackErrors   *types.CustomBool           `json:"ignore-unpack-errors,omitempty" url:"force,omitempty,int"`
+	Lock                 *string                     `json:"lock,omitempty"                 url:"lock,omitempty,int"`
+	MountPoints          CustomMountPoints           `json:"mp,omitempty"                   url:"mp,omitempty,numbered"`
+	PassthroughDevices   CustomPassthroughDevices    `json:"dev,omitempty"                  url:"dev,omitempty,numbered"`
+	NetworkInterfaces    CustomNetworkInterfaces     `json:"net,omitempty"                  url:"net,omitempty,numbered"`
+	OSTemplateFileVolume *string                     `json:"ostemplate,omitempty"           url:"ostemplate,omitempty"`
+	OSType               *string                     `json:"ostype,omitempty"               url:"ostype,omitempty"`
+	Password             *string                     `json:"password,omitempty"             url:"password,omitempty"`
+	PoolID               *string                     `json:"pool,omitempty"                 url:"pool,omitempty"`
+	Protection           *types.CustomBool           `json:"protection,omitempty"           url:"protection,omitempty,int"`
+	Restore              *types.CustomBool           `json:"restore,omitempty"              url:"restore,omitempty,int"`
+	RootFS               *CustomRootFS               `json:"rootfs,omitempty"               url:"rootfs,omitempty"`
+	SSHKeys              *CustomSSHKeys              `json:"ssh-public-keys,omitempty"      url:"ssh-public-keys,omitempty"`
+	Start                *types.CustomBool           `json:"start,omitempty"                url:"start,omitempty,int"`
+	StartOnBoot          *types.CustomBool           `json:"onboot,omitempty"               url:"onboot,omitempty,int"`
+	StartupBehavior      *CustomStartupBehavior      `json:"startup,omitempty"              url:"startup,omitempty"`
+	Swap                 *int                        `json:"swap,omitempty"                 url:"swap,omitempty"`
+	Tags                 *string                     `json:"tags,omitempty"                 url:"tags,omitempty"`
+	Template             *types.CustomBool           `json:"template,omitempty"             url:"template,omitempty,int"`
+	TTY                  *int                        `json:"tty,omitempty"                  url:"tty,omitempty"`
+	Unique               *types.CustomBool           `json:"unique,omitempty"               url:"unique,omitempty,int"`
+	Unprivileged         *types.CustomBool           `json:"unprivileged,omitempty"         url:"unprivileged,omitempty,int"`
+	VMID                 *int                        `json:"vmid,omitempty"                 url:"vmid,omitempty"`
 }
 
+// CustomEnvironmentVariables contains environment variable key-value pairs.
+type CustomEnvironmentVariables map[string]string
+
 // CustomFeatures contains the values for the "features" property.
 type CustomFeatures struct {
 	FUSE       *types.CustomBool `json:"fuse,omitempty"    url:"fuse,omitempty,int"`
@@ -189,35 +194,36 @@ type GetResponseBody struct {
 
 // GetResponseData contains the data from a user get response.
 type GetResponseData struct {
-	ConsoleEnabled     *types.CustomBool        `json:"console,omitempty"`
-	ConsoleMode        *string                  `json:"cmode,omitempty"`
-	CPUArchitecture    *string                  `json:"arch,omitempty"`
-	CPUCores           *int                     `json:"cores,omitempty"`
-	CPULimit           *types.CustomInt         `json:"cpulimit,omitempty"`
-	CPUUnits           *int                     `json:"cpuunits,omitempty"`
-	DedicatedMemory    *int                     `json:"memory,omitempty"`
-	Description        *string                  `json:"description,omitempty"`
-	Digest             string                   `json:"digest"`
-	DNSDomain          *string                  `json:"searchdomain,omitempty"`
-	DNSServer          *string                  `json:"nameserver,omitempty"`
-	Features           *CustomFeatures          `json:"features,omitempty"`
-	HookScript         *string                  `json:"hookscript,omitempty"`
-	Hostname           *string                  `json:"hostname,omitempty"`
-	Lock               *types.CustomBool        `json:"lock,omitempty"`
-	LXCConfiguration   *[][2]string             `json:"lxc,omitempty"`
-	MountPoints        CustomMountPoints        `json:"mp,omitempty"`
-	PassthroughDevices CustomPassthroughDevices `json:"dev,omitempty"`
-	NetworkInterfaces  CustomNetworkInterfaces  `json:"net,omitempty"`
-	OSType             *string                  `json:"ostype,omitempty"`
-	Protection         *types.CustomBool        `json:"protection,omitempty"`
-	RootFS             *CustomRootFS            `json:"rootfs,omitempty"`
-	StartOnBoot        *types.CustomBool        `json:"onboot,omitempty"`
-	StartupBehavior    *CustomStartupBehavior   `json:"startup,omitempty"`
-	Swap               *int                     `json:"swap,omitempty"`
-	Tags               *string                  `json:"tags,omitempty"`
-	Template           *types.CustomBool        `json:"template,omitempty"`
-	TTY                *int                     `json:"tty,omitempty"`
-	Unprivileged       *types.CustomBool        `json:"unprivileged,omitempty"`
+	ConsoleEnabled       *types.CustomBool           `json:"console,omitempty"`
+	ConsoleMode          *string                     `json:"cmode,omitempty"`
+	CPUArchitecture      *string                     `json:"arch,omitempty"`
+	CPUCores             *int                        `json:"cores,omitempty"`
+	CPULimit             *types.CustomInt            `json:"cpulimit,omitempty"`
+	CPUUnits             *int                        `json:"cpuunits,omitempty"`
+	DedicatedMemory      *int                        `json:"memory,omitempty"`
+	Description          *string                     `json:"description,omitempty"`
+	Digest               string                      `json:"digest"`
+	DNSDomain            *string                     `json:"searchdomain,omitempty"`
+	DNSServer            *string                     `json:"nameserver,omitempty"`
+	EnvironmentVariables *CustomEnvironmentVariables `json:"env,omitempty"`
+	Features             *CustomFeatures             `json:"features,omitempty"`
+	HookScript           *string                     `json:"hookscript,omitempty"`
+	Hostname             *string                     `json:"hostname,omitempty"`
+	Lock                 *types.CustomBool           `json:"lock,omitempty"`
+	LXCConfiguration     *[][2]string                `json:"lxc,omitempty"`
+	MountPoints          CustomMountPoints           `json:"mp,omitempty"`
+	PassthroughDevices   CustomPassthroughDevices    `json:"dev,omitempty"`
+	NetworkInterfaces    CustomNetworkInterfaces     `json:"net,omitempty"`
+	OSType               *string                     `json:"ostype,omitempty"`
+	Protection           *types.CustomBool           `json:"protection,omitempty"`
+	RootFS               *CustomRootFS               `json:"rootfs,omitempty"`
+	StartOnBoot          *types.CustomBool           `json:"onboot,omitempty"`
+	StartupBehavior      *CustomStartupBehavior      `json:"startup,omitempty"`
+	Swap                 *int                        `json:"swap,omitempty"`
+	Tags                 *string                     `json:"tags,omitempty"`
+	Template             *types.CustomBool           `json:"template,omitempty"`
+	TTY                  *int                        `json:"tty,omitempty"`
+	Unprivileged         *types.CustomBool           `json:"unprivileged,omitempty"`
 }
 
 // GetStatusResponseBody contains the body from a container get status response.
@@ -649,6 +655,31 @@ func (r *CustomStartupBehavior) EncodeValues(
 	return nil
 }
 
+// EncodeValues converts a CustomEnvironmentVariables map to a NUL-separated string.
+func (r CustomEnvironmentVariables) EncodeValues(key string, v *url.Values) error {
+	if len(r) == 0 {
+		return nil
+	}
+
+	keys := make([]string, 0, len(r))
+	for k := range r {
+		keys = append(keys, k)
+	}
+
+	sort.Strings(keys)
+
+	envVars := make([]string, 0, len(r))
+
+	for _, k := range keys {
+		envVars = append(envVars, fmt.Sprintf("%s=%s", k, r[k]))
+	}
+
+	// join with NUL character (ASCII 0)
+	v.Add(key, strings.Join(envVars, "\x00"))
+
+	return nil
+}
+
 // UnmarshalJSON converts a ContainerCustomFeatures string to an object.
 func (r *CustomFeatures) UnmarshalJSON(b []byte) error {
 	var s string
@@ -940,6 +971,30 @@ func (r *CustomStartupBehavior) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
+// UnmarshalJSON converts a CustomEnvironmentVariables NUL-separated string to a map.
+func (r *CustomEnvironmentVariables) UnmarshalJSON(b []byte) error {
+	var s string
+
+	if err := json.Unmarshal(b, &s); err != nil {
+		return fmt.Errorf("unable to unmarshal CustomEnvironmentVariables: %w", err)
+	}
+
+	*r = make(CustomEnvironmentVariables)
+
+	// Split by NUL character (ASCII 0)
+	pairs := strings.SplitSeq(s, "\x00")
+
+	for p := range pairs {
+		v := strings.SplitN(p, "=", 2)
+
+		if len(v) == 2 {
+			(*r)[v[0]] = v[1]
+		}
+	}
+
+	return nil
+}
+
 // UnmarshalJSON unmarshals the data from the JSON response, populating the CustomStorageDevices field.
 func (d *GetResponseData) UnmarshalJSON(b []byte) error {
 	type Alias GetResponseData
diff --git a/proxmoxtf/resource/container/container.go b/proxmoxtf/resource/container/container.go
index 76ca2ea4..d9561736 100644
--- a/proxmoxtf/resource/container/container.go
+++ b/proxmoxtf/resource/container/container.go
@@ -116,6 +116,7 @@ const (
 	mkDiskQuota                         = "quota"
 	mkDiskReplicate                     = "replicate"
 	mkDiskSize                          = "size"
+	mkEnvironmentVariables              = "environment_variables"
 	mkFeatures                          = "features"
 	mkFeaturesNesting                   = "nesting"
 	mkFeaturesKeyControl                = "keyctl"
@@ -397,6 +398,15 @@ func Container() *schema.Resource {
 				MaxItems: 1,
 				MinItems: 0,
 			},
+			mkEnvironmentVariables: {
+				Type:        schema.TypeMap,
+				Description: "The runtime environment variables for the container init process.",
+				Optional:    true,
+				Elem: &schema.Schema{
+					Type: schema.TypeString,
+				},
+				ValidateDiagFunc: EnvironmentVariablesValidator(),
+			},
 			mkFeatures: {
 				Type:        schema.TypeList,
 				Description: "Features",
@@ -1581,6 +1591,8 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m any) d
 
 	diskDatastoreID := diskBlock[mkDiskDatastoreID].(string)
 
+	environmentVariables := containerGetEnvironmentVariables(d)
+
 	features, err := containerGetFeatures(container, d)
 	if err != nil {
 		return diag.FromErr(err)
@@ -1978,6 +1990,10 @@ func containerCreateCustom(ctx context.Context, d *schema.ResourceData, m any) d
 		createBody.Tags = &tagsString
 	}
 
+	if environmentVariables != nil {
+		createBody.EnvironmentVariables = environmentVariables
+	}
+
 	err = client.Node(nodeName).Container(0).CreateContainer(ctx, &createBody)
 	if err != nil {
 		return diag.FromErr(err)
@@ -2021,6 +2037,20 @@ func containerCreateStart(ctx context.Context, d *schema.ResourceData, m any) di
 	return containerRead(ctx, d, m)
 }
 
+func containerGetEnvironmentVariables(d *schema.ResourceData) *containers.CustomEnvironmentVariables {
+	envVarsRaw := d.Get(mkEnvironmentVariables).(map[string]any)
+	if len(envVarsRaw) == 0 {
+		return nil
+	}
+
+	envVars := make(containers.CustomEnvironmentVariables)
+	for k, v := range envVarsRaw {
+		envVars[k] = v.(string)
+	}
+
+	return &envVars
+}
+
 // NOTE: this function is NOT used in `read`!
 func containerGetExistingNetworkInterface(
 	ctx context.Context,
@@ -2834,6 +2864,17 @@ func containerRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diag
 		diags = append(diags, diag.FromErr(e)...)
 	}
 
+	envVarsMap := make(map[string]any)
+
+	if containerConfig.EnvironmentVariables != nil {
+		for k, v := range *containerConfig.EnvironmentVariables {
+			envVarsMap[k] = v
+		}
+	}
+
+	e = d.Set(mkEnvironmentVariables, envVarsMap)
+	diags = append(diags, diag.FromErr(e)...)
+
 	if len(clone) == 0 || template {
 		if containerConfig.Template != nil {
 			e = d.Set(
@@ -3040,6 +3081,17 @@ func containerUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Di
 		rebootRequired = true
 	}
 
+	if d.HasChange(mkEnvironmentVariables) {
+		environmentVariables := containerGetEnvironmentVariables(d)
+		if environmentVariables != nil {
+			updateBody.EnvironmentVariables = environmentVariables
+		} else {
+			updateBody.Delete = append(updateBody.Delete, "env")
+		}
+
+		rebootRequired = true
+	}
+
 	if d.HasChange(mkFeatures) {
 		features, err := containerGetFeatures(container, d)
 		if err != nil {
diff --git a/proxmoxtf/resource/container/validators.go b/proxmoxtf/resource/container/validators.go
index d41705f9..8bfce12c 100644
--- a/proxmoxtf/resource/container/validators.go
+++ b/proxmoxtf/resource/container/validators.go
@@ -7,6 +7,9 @@
 package resource
 
 import (
+	"fmt"
+	"regexp"
+
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
 )
@@ -54,3 +57,34 @@ func OperatingSystemTypeValidator() schema.SchemaValidateDiagFunc {
 		"unmanaged",
 	}, false))
 }
+
+// EnvironmentVariablesValidator validates environment variable map based on Proxmox API requirements.
+func EnvironmentVariablesValidator() schema.SchemaValidateDiagFunc {
+	// Proxmox regex for keys: \w+ (one or more word characters)
+	keyRegex := regexp.MustCompile(`^\w+$`)
+
+	// Proxmox rejects these control characters in values: \x00-\x08, \x10-\x1F, \x7F
+	invalidValueCharsRegex := regexp.MustCompile(`[\x00-\x08\x10-\x1F\x7F]`)
+
+	return validation.ToDiagFunc(func(i any, k string) ([]string, []error) {
+		var es []error
+
+		envVars, ok := i.(map[string]any)
+		if !ok {
+			es = append(es, fmt.Errorf("expected type of %s to be map[string]any", k))
+			return nil, es
+		}
+
+		for key, val := range envVars {
+			if !keyRegex.MatchString(key) {
+				es = append(es, fmt.Errorf("environment variable key '%s' is invalid. Keys must contain only letters, digits, and underscores", key))
+			}
+
+			if valStr, ok := val.(string); ok && invalidValueCharsRegex.MatchString(valStr) {
+				es = append(es, fmt.Errorf("environment variable '%s' has an invalid value. Values cannot contain control characters", key))
+			}
+		}
+
+		return nil, es
+	})
+}



Test patch:
diff --git a/fwprovider/test/resource_container_test.go b/fwprovider/test/resource_container_test.go
index 92dde8e1..be70ec30 100644
--- a/fwprovider/test/resource_container_test.go
+++ b/fwprovider/test/resource_container_test.go
@@ -941,6 +941,166 @@ func TestAccResourceContainerIpv4Ipv6(t *testing.T) {
 	})
 }
 
+func TestAccResourceContainerEnvironmentVariables(t *testing.T) {
+	te := InitEnvironment(t)
+	accTestContainerID := 100000 + rand.Intn(99999)
+	imageFileName := fmt.Sprintf("%d-alpine-3.22-default_20250617_amd64.tar.xz", time.Now().UnixMicro())
+
+	testAccDownloadContainerTemplate(t, te, imageFileName)
+
+	te.AddTemplateVars(map[string]interface{}{
+		"ImageFileName":   imageFileName,
+		"TestContainerID": accTestContainerID,
+	})
+
+	resource.ParallelTest(t, resource.TestCase{
+		ProtoV6ProviderFactories: te.AccProviders,
+		Steps: []resource.TestStep{
+			{
+				Config: te.RenderConfig(`
+				resource "proxmox_virtual_environment_container" "test_container" {
+					node_name = "{{.NodeName}}"
+					vm_id     = {{.TestContainerID}}
+					unprivileged = true
+					disk {
+						datastore_id = "local-lvm"
+						size         = 4
+					}
+					initialization {
+						hostname = "test-env-vars"
+						ip_config {
+							ipv4 {
+								address = "dhcp"
+							}
+						}
+					}
+					network_interface {
+						name = "vmbr0"
+					}
+					operating_system {
+						template_file_id = "local:vztmpl/{{.ImageFileName}}"
+						type             = "alpine"
+					}
+					environment_variables = {
+						FOO = "bar"
+						BAZ = "qux"
+					}
+				}`, WithRootUser()),
+				Check: resource.ComposeTestCheckFunc(
+					ResourceAttributes(accTestContainerName, map[string]string{
+						"initialization.0.hostname": "test-env-vars",
+						"environment_variables.FOO": "bar",
+						"environment_variables.BAZ": "qux",
+						"environment_variables.%":   "2",
+					}),
+					func(*terraform.State) error {
+						ct := te.NodeClient().Container(accTestContainerID)
+						ctInfo, err := ct.GetContainer(t.Context())
+						require.NoError(te.t, err, "failed to get container")
+						require.NotNil(te.t, ctInfo.EnvironmentVariables, "environment_variables should not be nil")
+						require.Equal(te.t, "bar", (*ctInfo.EnvironmentVariables)["FOO"])
+						require.Equal(te.t, "qux", (*ctInfo.EnvironmentVariables)["BAZ"])
+
+						return nil
+					},
+				),
+			},
+			{
+				Config: te.RenderConfig(`
+				resource "proxmox_virtual_environment_container" "test_container" {
+					node_name = "{{.NodeName}}"
+					vm_id     = {{.TestContainerID}}
+					unprivileged = true
+					disk {
+						datastore_id = "local-lvm"
+						size         = 4
+					}
+					initialization {
+						hostname = "test-env-vars"
+						ip_config {
+							ipv4 {
+								address = "dhcp"
+							}
+						}
+					}
+					network_interface {
+						name = "vmbr0"
+					}
+					operating_system {
+						template_file_id = "local:vztmpl/{{.ImageFileName}}"
+						type             = "alpine"
+					}
+					environment_variables = {
+						FOO = "updated"
+						NEW_VAR = "new_value"
+					}
+				}`, WithRootUser()),
+				Check: resource.ComposeTestCheckFunc(
+					ResourceAttributes(accTestContainerName, map[string]string{
+						"environment_variables.FOO":     "updated",
+						"environment_variables.NEW_VAR": "new_value",
+						"environment_variables.%":       "2",
+					}),
+					func(*terraform.State) error {
+						ct := te.NodeClient().Container(accTestContainerID)
+						ctInfo, err := ct.GetContainer(t.Context())
+						require.NoError(te.t, err, "failed to get container")
+						require.NotNil(te.t, ctInfo.EnvironmentVariables, "environment_variables should not be nil")
+						require.Equal(te.t, "updated", (*ctInfo.EnvironmentVariables)["FOO"])
+						require.Equal(te.t, "new_value", (*ctInfo.EnvironmentVariables)["NEW_VAR"])
+						_, hasBaz := (*ctInfo.EnvironmentVariables)["BAZ"]
+						require.False(te.t, hasBaz, "BAZ should be removed")
+
+						return nil
+					},
+				),
+			},
+			{
+				Config: te.RenderConfig(`
+				resource "proxmox_virtual_environment_container" "test_container" {
+					node_name = "{{.NodeName}}"
+					vm_id     = {{.TestContainerID}}
+					unprivileged = true
+					disk {
+						datastore_id = "local-lvm"
+						size         = 4
+					}
+					initialization {
+						hostname = "test-env-vars"
+						ip_config {
+							ipv4 {
+								address = "dhcp"
+							}
+						}
+					}
+					network_interface {
+						name = "vmbr0"
+					}
+					operating_system {
+						template_file_id = "local:vztmpl/{{.ImageFileName}}"
+						type             = "alpine"
+					}
+				}`, WithRootUser()),
+				Check: resource.ComposeTestCheckFunc(
+					ResourceAttributes(accTestContainerName, map[string]string{
+						"environment_variables.%": "0",
+					}),
+					func(*terraform.State) error {
+						ct := te.NodeClient().Container(accTestContainerID)
+						ctInfo, err := ct.GetContainer(t.Context())
+						require.NoError(te.t, err, "failed to get container")
+						if ctInfo.EnvironmentVariables != nil {
+							require.Empty(te.t, *ctInfo.EnvironmentVariables, "environment_variables should be empty")
+						}
+
+						return nil
+					},
+				),
+			},
+		},
+	})
+}
+
 func testAccDownloadContainerTemplate(t *testing.T, te *Environment, imageFileName string) {
 	t.Helper()
 	err := te.NodeStorageClient().DownloadFileByURL(context.Background(), &storage.DownloadURLPostRequestBody{
diff --git a/proxmoxtf/resource/container/container_test.go b/proxmoxtf/resource/container/container_test.go
index 0aabf09c..275714d4 100644
--- a/proxmoxtf/resource/container/container_test.go
+++ b/proxmoxtf/resource/container/container_test.go
@@ -41,6 +41,7 @@ func TestContainerSchema(t *testing.T) {
 		mkCPU,
 		mkDescription,
 		mkDisk,
+		mkEnvironmentVariables,
 		mkInitialization,
 		mkHookScriptFileID,
 		mkMemory,
@@ -59,24 +60,25 @@ func TestContainerSchema(t *testing.T) {
 	})
 
 	test.AssertValueTypes(t, s, map[string]schema.ValueType{
-		mkCPU:               schema.TypeList,
-		mkDescription:       schema.TypeString,
-		mkDisk:              schema.TypeList,
-		mkInitialization:    schema.TypeList,
-		mkHookScriptFileID:  schema.TypeString,
-		mkMemory:            schema.TypeList,
-		mkDevicePassthrough: schema.TypeList,
-		mkMountPoint:        schema.TypeList,
-		mkOperatingSystem:   schema.TypeList,
-		mkPoolID:            schema.TypeString,
-		mkProtection:        schema.TypeBool,
-		mkStarted:           schema.TypeBool,
-		mkTags:              schema.TypeList,
-		mkTemplate:          schema.TypeBool,
-		mkUnprivileged:      schema.TypeBool,
-		mkStartOnBoot:       schema.TypeBool,
-		mkFeatures:          schema.TypeList,
-		mkVMID:              schema.TypeInt,
+		mkCPU:                  schema.TypeList,
+		mkDescription:          schema.TypeString,
+		mkDisk:                 schema.TypeList,
+		mkEnvironmentVariables: schema.TypeMap,
+		mkInitialization:       schema.TypeList,
+		mkHookScriptFileID:     schema.TypeString,
+		mkMemory:               schema.TypeList,
+		mkDevicePassthrough:    schema.TypeList,
+		mkMountPoint:           schema.TypeList,
+		mkOperatingSystem:      schema.TypeList,
+		mkPoolID:               schema.TypeString,
+		mkProtection:           schema.TypeBool,
+		mkStarted:              schema.TypeBool,
+		mkTags:                 schema.TypeList,
+		mkTemplate:             schema.TypeBool,
+		mkUnprivileged:         schema.TypeBool,
+		mkStartOnBoot:          schema.TypeBool,
+		mkFeatures:             schema.TypeList,
+		mkVMID:                 schema.TypeInt,
 	})
 
 	cloneSchema := test.AssertNestedSchemaExistence(t, s, mkClone)